From d06017f86bd544f31bd40fc3b6f946d3c7af7c65 Mon Sep 17 00:00:00 2001 From: "David J. Malan" <malan@harvard.edu> Date: Sun, 5 Jun 2022 21:12:03 -0400 Subject: [PATCH 01/44] using INFO, WARNING, and ERROR instead of DEBUG --- setup.py | 2 +- src/cs50/sql.py | 6 +++--- tests/foo.py | 11 ++++++----- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/setup.py b/setup.py index d1c4f9b..c363ff1 100644 --- a/setup.py +++ b/setup.py @@ -16,5 +16,5 @@ package_dir={"": "src"}, packages=["cs50"], url="https://github.com/cs50/python-cs50", - version="9.0.0" + version="10.0.0" ) diff --git a/src/cs50/sql.py b/src/cs50/sql.py index b0aa94e..8f0a1be 100644 --- a/src/cs50/sql.py +++ b/src/cs50/sql.py @@ -384,7 +384,7 @@ def teardown_appcontext(exception): # If constraint violated, return None except sqlalchemy.exc.IntegrityError as e: - self._logger.debug(termcolor.colored(statement, "yellow")) + self._logger.warning(termcolor.colored(statement, "yellow")) e = ValueError(e.orig) e.__cause__ = None raise e @@ -392,14 +392,14 @@ def teardown_appcontext(exception): # If user error except (sqlalchemy.exc.OperationalError, sqlalchemy.exc.ProgrammingError) as e: self._disconnect() - self._logger.debug(termcolor.colored(statement, "red")) + self._logger.error(termcolor.colored(statement, "red")) e = RuntimeError(e.orig) e.__cause__ = None raise e # Return value else: - self._logger.debug(termcolor.colored(_statement, "green")) + self._logger.info(termcolor.colored(_statement, "green")) if self._autocommit: # Don't stay connected unnecessarily self._disconnect() return ret diff --git a/tests/foo.py b/tests/foo.py index 7f32a00..f3955fc 100644 --- a/tests/foo.py +++ b/tests/foo.py @@ -5,23 +5,23 @@ import cs50 -""" db = cs50.SQL("sqlite:///foo.db") logging.getLogger("cs50").disabled = False +logging.getLogger("cs50").setLevel(logging.ERROR) -#db.execute("SELECT ? FROM ? ORDER BY ?", "a", "tbl", "c") -db.execute("CREATE TABLE IF NOT EXISTS bar (firstname STRING)") +db.execute("CREATE TABLE IF NOT EXISTS bar (firstname STRING UNIQUE)") +db.execute("INSERT INTO bar VALUES (?)", "baz") db.execute("INSERT INTO bar VALUES (?)", "baz") db.execute("INSERT INTO bar VALUES (?)", "qux") db.execute("SELECT * FROM bar WHERE firstname IN (?)", ("baz", "qux")) db.execute("DELETE FROM bar") + """ db = cs50.SQL("postgresql://postgres@localhost/test") -""" print(db.execute("DROP TABLE IF EXISTS cs50")) print(db.execute("CREATE TABLE cs50 (id SERIAL PRIMARY KEY, val VARCHAR(16), bin BYTEA)")) print(db.execute("INSERT INTO cs50 (val) VALUES('foo')")) @@ -31,7 +31,6 @@ print(db.execute("CREATE TABLE cs50 (val VARCHAR(16), bin BYTEA)")) print(db.execute("INSERT INTO cs50 (val) VALUES('foo')")) print(db.execute("SELECT * FROM cs50")) -""" print(db.execute("DROP TABLE IF EXISTS cs50")) print(db.execute("CREATE TABLE cs50 (id SERIAL PRIMARY KEY, val VARCHAR(16), bin BYTEA)")) @@ -46,3 +45,5 @@ pass print(db.execute("INSERT INTO cs50 (val) VALUES('qux')")) #print(db.execute("DELETE FROM cs50")) + +""" From afb51bc6163e9e82653e1aa251cd0e46afc10515 Mon Sep 17 00:00:00 2001 From: "David J. Malan" <malan@harvard.edu> Date: Mon, 6 Jun 2022 09:18:25 -0400 Subject: [PATCH 02/44] changing minor version instead --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index c363ff1..0eb27bf 100644 --- a/setup.py +++ b/setup.py @@ -16,5 +16,5 @@ package_dir={"": "src"}, packages=["cs50"], url="https://github.com/cs50/python-cs50", - version="10.0.0" + version="9.1.0" ) From 6a5bee036c16be15576c815c1918651f00fc8cca Mon Sep 17 00:00:00 2001 From: "David J. Malan" <malan@harvard.edu> Date: Mon, 6 Jun 2022 09:21:27 -0400 Subject: [PATCH 03/44] changed warning to error --- src/cs50/sql.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cs50/sql.py b/src/cs50/sql.py index 8f0a1be..cd631fc 100644 --- a/src/cs50/sql.py +++ b/src/cs50/sql.py @@ -382,9 +382,9 @@ def teardown_appcontext(exception): elif command in ["DELETE", "UPDATE"]: ret = result.rowcount - # If constraint violated, return None + # If constraint violated except sqlalchemy.exc.IntegrityError as e: - self._logger.warning(termcolor.colored(statement, "yellow")) + self._logger.error(termcolor.colored(statement, "red")) e = ValueError(e.orig) e.__cause__ = None raise e From 42482f9f9290501f45a6ff7552895d4fdef24e16 Mon Sep 17 00:00:00 2001 From: "David J. Malan" <malan@harvard.edu> Date: Mon, 6 Jun 2022 09:31:01 -0400 Subject: [PATCH 04/44] logging bytes-abbreviated _statement instead --- src/cs50/sql.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cs50/sql.py b/src/cs50/sql.py index cd631fc..c6cdc4b 100644 --- a/src/cs50/sql.py +++ b/src/cs50/sql.py @@ -384,7 +384,7 @@ def teardown_appcontext(exception): # If constraint violated except sqlalchemy.exc.IntegrityError as e: - self._logger.error(termcolor.colored(statement, "red")) + self._logger.error(termcolor.colored(_statement, "red")) e = ValueError(e.orig) e.__cause__ = None raise e @@ -392,7 +392,7 @@ def teardown_appcontext(exception): # If user error except (sqlalchemy.exc.OperationalError, sqlalchemy.exc.ProgrammingError) as e: self._disconnect() - self._logger.error(termcolor.colored(statement, "red")) + self._logger.error(termcolor.colored(_statement, "red")) e = RuntimeError(e.orig) e.__cause__ = None raise e From a43b565b6ae9199d062336edbc670fbb7b184fbc Mon Sep 17 00:00:00 2001 From: "David J. Malan" <malan@harvard.edu> Date: Mon, 6 Jun 2022 21:34:00 -0400 Subject: [PATCH 05/44] scoping INFO and ERROR to FLASK_ENV=development --- setup.py | 2 +- src/cs50/sql.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 0eb27bf..765213a 100644 --- a/setup.py +++ b/setup.py @@ -16,5 +16,5 @@ package_dir={"": "src"}, packages=["cs50"], url="https://github.com/cs50/python-cs50", - version="9.1.0" + version="9.2.0" ) diff --git a/src/cs50/sql.py b/src/cs50/sql.py index c6cdc4b..35c180e 100644 --- a/src/cs50/sql.py +++ b/src/cs50/sql.py @@ -9,6 +9,7 @@ def _enable_logging(f): import logging import functools + import os @functools.wraps(f) def decorator(*args, **kwargs): @@ -19,9 +20,9 @@ def decorator(*args, **kwargs): except ModuleNotFoundError: return f(*args, **kwargs) - # Enable logging + # Enable logging in development mode disabled = logging.getLogger("cs50").disabled - if flask.current_app: + if flask.current_app and os.getenv("FLASK_ENV") == "development": logging.getLogger("cs50").disabled = False try: return f(*args, **kwargs) From a6ceac6f47e5c77fc814b35ebcf66e1de9ce9cc8 Mon Sep 17 00:00:00 2001 From: "David J. Malan" <malan@harvard.edu> Date: Tue, 14 Jun 2022 20:18:26 -0400 Subject: [PATCH 06/44] fixed support for None as NULL --- setup.py | 2 +- src/cs50/sql.py | 2 +- tests/foo.py | 8 +++++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index 765213a..f0edaaf 100644 --- a/setup.py +++ b/setup.py @@ -16,5 +16,5 @@ package_dir={"": "src"}, packages=["cs50"], url="https://github.com/cs50/python-cs50", - version="9.2.0" + version="9.2.1" ) diff --git a/src/cs50/sql.py b/src/cs50/sql.py index 35c180e..f008b6f 100644 --- a/src/cs50/sql.py +++ b/src/cs50/sql.py @@ -476,7 +476,7 @@ def __escape(value): elif value is None: return sqlparse.sql.Token( sqlparse.tokens.Keyword, - sqlalchemy.types.NullType().literal_processor(self._engine.dialect)(value)) + sqlalchemy.null()) # Unsupported value else: diff --git a/tests/foo.py b/tests/foo.py index f3955fc..2cf74e9 100644 --- a/tests/foo.py +++ b/tests/foo.py @@ -10,13 +10,15 @@ logging.getLogger("cs50").disabled = False logging.getLogger("cs50").setLevel(logging.ERROR) -db.execute("CREATE TABLE IF NOT EXISTS bar (firstname STRING UNIQUE)") +db.execute("DROP TABLE IF EXISTS bar") +db.execute("CREATE TABLE bar (firstname STRING UNIQUE)") -db.execute("INSERT INTO bar VALUES (?)", "baz") +db.execute("INSERT INTO bar VALUES (?)", None) db.execute("INSERT INTO bar VALUES (?)", "baz") db.execute("INSERT INTO bar VALUES (?)", "qux") db.execute("SELECT * FROM bar WHERE firstname IN (?)", ("baz", "qux")) -db.execute("DELETE FROM bar") +print(db.execute("SELECT * FROM bar")) +#db.execute("DELETE FROM bar") """ From 0039f7e2ba76f561ac5eaf93d4f63d160c660464 Mon Sep 17 00:00:00 2001 From: "David J. Malan" <malan@harvard.edu> Date: Tue, 14 Jun 2022 20:26:16 -0400 Subject: [PATCH 07/44] fixed license --- setup.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f0edaaf..0c3f650 100644 --- a/setup.py +++ b/setup.py @@ -12,9 +12,11 @@ description="CS50 library for Python", install_requires=["Flask>=1.0", "SQLAlchemy", "sqlparse", "termcolor", "wheel"], keywords="cs50", + license="GPLv3", + long_description_content_type="text/markdown", name="cs50", package_dir={"": "src"}, packages=["cs50"], url="https://github.com/cs50/python-cs50", - version="9.2.1" + version="9.2.2" ) From 7448bc073000c1e3a9f0cd1e690eb45e4ad9537e Mon Sep 17 00:00:00 2001 From: Rongxin Liu <rongxinliu.dev@gmail.com> Date: Sat, 16 Jul 2022 15:50:55 -0400 Subject: [PATCH 08/44] added github release automation to workflow --- .github/workflows/main.yml | 40 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e32f995..b0f91a9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -28,18 +28,58 @@ jobs: run: | pip install . pip install mysqlclient psycopg2-binary + - name: Run tests run: python tests/sql.py env: MYSQL_HOST: 127.0.0.1 POSTGRESQL_HOST: 127.0.0.1 + - name: Install pypa/build run: python -m pip install build --user + - name: Build a binary wheel and a source tarball run: python -m build --sdist --wheel --outdir dist/ . + - name: Deploy to PyPI if: ${{ github.ref == 'refs/heads/main' }} uses: pypa/gh-action-pypi-publish@release/v1 with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} + + - name: Get Version + id: py_version + run: | + echo ::set-output name=version::$(python3 setup.py --version) + + - name: Create Tag + uses: actions/github-script@v6 + with: + github-token: ${{ github.token }} + script: | + try { + await github.rest.git.updateRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: "tags/v${{ steps.py_version.outputs.version }}", + sha: context.sha, + force: true + }) + } catch (e) { + await github.rest.git.createRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: "refs/tags/v${{ steps.py_version.outputs.version }}", + sha: context.sha + }) + } + + - name: Create Release + run: | + curl \ + -X POST \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: token ${{ secrets.GH_RELEASE_TOKEN }}" \ + https://api.github.com/repos/${GITHUB_REPOSITORY}/releases \ + -d '{"tag_name":"v${{ steps.py_version.outputs.version }}","target_commitish":"${{ github.sha }}","name":"v${{ steps.py_version.outputs.version }}","body":"${{ github.event.head_commit.message }}","draft":false,"prerelease":false,"generate_release_notes":false}' From 23531820daf415732e07f39ce546f173e1da311a Mon Sep 17 00:00:00 2001 From: Rongxin Liu <rongxinliu.dev@gmail.com> Date: Thu, 1 Sep 2022 13:19:58 -0400 Subject: [PATCH 09/44] use v3 actions for checkout and setup-python --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b0f91a9..dd919ef 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -20,8 +20,8 @@ jobs: ports: - 5432:5432 steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-python@v3 with: python-version: '3.6' - name: Setup databases From 07ad96b72376ece9a6abeb7172ea0650b89b9580 Mon Sep 17 00:00:00 2001 From: Rongxin Liu <rongxinliu.dev@gmail.com> Date: Sat, 22 Oct 2022 09:15:54 -0400 Subject: [PATCH 10/44] remove travis badge --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index cf2c62d..c94bd1f 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,5 @@ # CS50 Library for Python -[](https://travis-ci.org/cs50/python-cs50) - ## Installation ``` From fcc68a16e7eaed3ec6899a487d18d01410ce8a0a Mon Sep 17 00:00:00 2001 From: Rongxin Liu <rongxinliu.dev@gmail.com> Date: Sat, 19 Nov 2022 13:52:16 -0500 Subject: [PATCH 11/44] simplified release process --- .github/workflows/main.yml | 33 +++++++-------------------------- 1 file changed, 7 insertions(+), 26 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index dd919ef..2059c50 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -53,33 +53,14 @@ jobs: run: | echo ::set-output name=version::$(python3 setup.py --version) - - name: Create Tag + - name: Create Release uses: actions/github-script@v6 with: github-token: ${{ github.token }} script: | - try { - await github.rest.git.updateRef({ - owner: context.repo.owner, - repo: context.repo.repo, - ref: "tags/v${{ steps.py_version.outputs.version }}", - sha: context.sha, - force: true - }) - } catch (e) { - await github.rest.git.createRef({ - owner: context.repo.owner, - repo: context.repo.repo, - ref: "refs/tags/v${{ steps.py_version.outputs.version }}", - sha: context.sha - }) - } - - - name: Create Release - run: | - curl \ - -X POST \ - -H "Accept: application/vnd.github+json" \ - -H "Authorization: token ${{ secrets.GH_RELEASE_TOKEN }}" \ - https://api.github.com/repos/${GITHUB_REPOSITORY}/releases \ - -d '{"tag_name":"v${{ steps.py_version.outputs.version }}","target_commitish":"${{ github.sha }}","name":"v${{ steps.py_version.outputs.version }}","body":"${{ github.event.head_commit.message }}","draft":false,"prerelease":false,"generate_release_notes":false}' + github.rest.repos.createRelease({ + owner: context.repo.owner, + repo: context.repo.repo, + tag_name: "v${{ steps.py_version.outputs.version }}", + tag_commitish: "${{ github.sha }}" + }) From b048ba21a2ad22058bac00f42be43b61f7fc744b Mon Sep 17 00:00:00 2001 From: Matthias Wenz <matthiaswenz@github.com> Date: Fri, 2 Dec 2022 00:25:51 +0100 Subject: [PATCH 12/44] Load sqlite module if sqlite connection --- src/cs50/sql.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cs50/sql.py b/src/cs50/sql.py index f008b6f..c5b2d94 100644 --- a/src/cs50/sql.py +++ b/src/cs50/sql.py @@ -51,12 +51,12 @@ def __init__(self, url, **kwargs): import re import sqlalchemy import sqlalchemy.orm - import sqlite3 import threading # Require that file already exist for SQLite matches = re.search(r"^sqlite:///(.+)$", url) if matches: + import sqlite3 if not os.path.exists(matches.group(1)): raise RuntimeError("does not exist: {}".format(matches.group(1))) if not os.path.isfile(matches.group(1)): From bd25298787cb55772f0a615eab5da8bbfc035683 Mon Sep 17 00:00:00 2001 From: Matthias Wenz <matthiaswenz@github.com> Date: Fri, 2 Dec 2022 16:57:01 +0100 Subject: [PATCH 13/44] Verify module loaded when accessing contents --- src/cs50/sql.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cs50/sql.py b/src/cs50/sql.py index c5b2d94..6611e49 100644 --- a/src/cs50/sql.py +++ b/src/cs50/sql.py @@ -74,7 +74,7 @@ def __init__(self, url, **kwargs): def connect(dbapi_connection, connection_record): # Enable foreign key constraints - if type(dbapi_connection) is sqlite3.Connection: # If back end is sqlite + if 'sqlite3' in sys.modules and type(dbapi_connection) is sqlite3.Connection: # If back end is sqlite cursor = dbapi_connection.cursor() cursor.execute("PRAGMA foreign_keys=ON") cursor.close() From 01236bcbc85191a7ed623f57d4c287e5ffb831e7 Mon Sep 17 00:00:00 2001 From: Rongxin Liu <rongxinliu.dev@gmail.com> Date: Fri, 2 Dec 2022 14:41:20 -0500 Subject: [PATCH 14/44] import sys module, tweaked styles --- src/cs50/sql.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/cs50/sql.py b/src/cs50/sql.py index 6611e49..1d33edb 100644 --- a/src/cs50/sql.py +++ b/src/cs50/sql.py @@ -1,3 +1,4 @@ +import sys import threading # Thread-local data @@ -74,7 +75,7 @@ def __init__(self, url, **kwargs): def connect(dbapi_connection, connection_record): # Enable foreign key constraints - if 'sqlite3' in sys.modules and type(dbapi_connection) is sqlite3.Connection: # If back end is sqlite + if "sqlite3" in sys.modules and type(dbapi_connection) is sqlite3.Connection: # If back end is sqlite cursor = dbapi_connection.cursor() cursor.execute("PRAGMA foreign_keys=ON") cursor.close() From a3784cc827f2fe616538131b8ce5f00357d95625 Mon Sep 17 00:00:00 2001 From: Rongxin Liu <rongxinliu.dev@gmail.com> Date: Fri, 2 Dec 2022 14:46:06 -0500 Subject: [PATCH 15/44] bump version number --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 0c3f650..2d90788 100644 --- a/setup.py +++ b/setup.py @@ -18,5 +18,5 @@ package_dir={"": "src"}, packages=["cs50"], url="https://github.com/cs50/python-cs50", - version="9.2.2" + version="9.2.3" ) From 328a76c0b5af368ecba32005c6bfba2afd23daa4 Mon Sep 17 00:00:00 2001 From: Rongxin Liu <rongxinliu.dev@gmail.com> Date: Fri, 2 Dec 2022 14:51:48 -0500 Subject: [PATCH 16/44] update setup-python action --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2059c50..26e072d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -21,7 +21,7 @@ jobs: - 5432:5432 steps: - uses: actions/checkout@v3 - - uses: actions/setup-python@v3 + - uses: actions/setup-python@v4 with: python-version: '3.6' - name: Setup databases From 0c552d363b2b6122484a056615800244f0026824 Mon Sep 17 00:00:00 2001 From: Rongxin Liu <rongxinliu.dev@gmail.com> Date: Fri, 2 Dec 2022 15:22:59 -0500 Subject: [PATCH 17/44] use try-catch import sqlite3, update action --- .github/workflows/main.yml | 4 +++- src/cs50/sql.py | 9 +++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 26e072d..f438d7b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -23,7 +23,8 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: - python-version: '3.6' + python-version: '3.7' + check-latest: true - name: Setup databases run: | pip install . @@ -54,6 +55,7 @@ jobs: echo ::set-output name=version::$(python3 setup.py --version) - name: Create Release + if: ${{ github.ref == 'refs/heads/main' }} uses: actions/github-script@v6 with: github-token: ${{ github.token }} diff --git a/src/cs50/sql.py b/src/cs50/sql.py index 1d33edb..8087657 100644 --- a/src/cs50/sql.py +++ b/src/cs50/sql.py @@ -53,11 +53,16 @@ def __init__(self, url, **kwargs): import sqlalchemy import sqlalchemy.orm import threading + + # Temporary fix for missing sqlite3 module on the buildpack stack + try: + import sqlite3 + except: + pass # Require that file already exist for SQLite matches = re.search(r"^sqlite:///(.+)$", url) if matches: - import sqlite3 if not os.path.exists(matches.group(1)): raise RuntimeError("does not exist: {}".format(matches.group(1))) if not os.path.isfile(matches.group(1)): @@ -75,7 +80,7 @@ def __init__(self, url, **kwargs): def connect(dbapi_connection, connection_record): # Enable foreign key constraints - if "sqlite3" in sys.modules and type(dbapi_connection) is sqlite3.Connection: # If back end is sqlite + if type(dbapi_connection) is sqlite3.Connection: # If back end is sqlite cursor = dbapi_connection.cursor() cursor.execute("PRAGMA foreign_keys=ON") cursor.close() From 3ccbd99c58d18df7e9385d421e85e8b7631bbe9d Mon Sep 17 00:00:00 2001 From: Rongxin Liu <rongxinliu.dev@gmail.com> Date: Fri, 2 Dec 2022 15:32:18 -0500 Subject: [PATCH 18/44] added try-catch workaround for SQL.connect when sqlite3 is not available --- setup.py | 2 +- src/cs50/sql.py | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index 2d90788..62a7abe 100644 --- a/setup.py +++ b/setup.py @@ -18,5 +18,5 @@ package_dir={"": "src"}, packages=["cs50"], url="https://github.com/cs50/python-cs50", - version="9.2.3" + version="9.2.4" ) diff --git a/src/cs50/sql.py b/src/cs50/sql.py index 8087657..f2c090d 100644 --- a/src/cs50/sql.py +++ b/src/cs50/sql.py @@ -80,10 +80,14 @@ def __init__(self, url, **kwargs): def connect(dbapi_connection, connection_record): # Enable foreign key constraints - if type(dbapi_connection) is sqlite3.Connection: # If back end is sqlite - cursor = dbapi_connection.cursor() - cursor.execute("PRAGMA foreign_keys=ON") - cursor.close() + try: + if type(dbapi_connection) is sqlite3.Connection: # If back end is sqlite + cursor = dbapi_connection.cursor() + cursor.execute("PRAGMA foreign_keys=ON") + cursor.close() + except: + # Temporary fix for missing sqlite3 module on the buildpack stack + pass # Register listener sqlalchemy.event.listen(self._engine, "connect", connect) From 777b4da5a1d2117b3959f72fbe74e4b50c2885de Mon Sep 17 00:00:00 2001 From: up-n-atom <adam.jaremko@gmail.com> Date: Sat, 28 Jan 2023 23:26:06 -0500 Subject: [PATCH 19/44] Respect pep8 and revert 659c8f4 As described in pep8: "Object type comparisons should always use isinstance() instead of comparing types directly:" Ref. https://peps.python.org/pep-0008/ --- src/cs50/cs50.py | 2 +- src/cs50/sql.py | 24 ++++++++++++------------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/cs50/cs50.py b/src/cs50/cs50.py index 1d7b6ea..16bfd0b 100644 --- a/src/cs50/cs50.py +++ b/src/cs50/cs50.py @@ -135,7 +135,7 @@ def get_string(prompt): as line endings. If user inputs only a line ending, returns "", not None. Returns None upon error or no input whatsoever (i.e., just EOF). """ - if type(prompt) is not str: + if not isinstance(prompt, str): raise TypeError("prompt must be of type str") try: return input(prompt) diff --git a/src/cs50/sql.py b/src/cs50/sql.py index f2c090d..4f9457b 100644 --- a/src/cs50/sql.py +++ b/src/cs50/sql.py @@ -81,7 +81,7 @@ def connect(dbapi_connection, connection_record): # Enable foreign key constraints try: - if type(dbapi_connection) is sqlite3.Connection: # If back end is sqlite + if isinstance(dbapi_connection, sqlite3.Connection): # If back end is sqlite cursor = dbapi_connection.cursor() cursor.execute("PRAGMA foreign_keys=ON") cursor.close() @@ -350,11 +350,11 @@ def teardown_appcontext(exception): # Coerce decimal.Decimal objects to float objects # https://groups.google.com/d/msg/sqlalchemy/0qXMYJvq8SA/oqtvMD9Uw-kJ - if type(row[column]) is decimal.Decimal: + if isinstance(row[column], decimal.Decimal): row[column] = float(row[column]) # Coerce memoryview objects (as from PostgreSQL's bytea columns) to bytes - elif type(row[column]) is memoryview: + elif isinstance(row[column], memoryview): row[column] = bytes(row[column]) # Rows to be returned @@ -432,13 +432,13 @@ def __escape(value): import sqlalchemy # bool - if type(value) is bool: + if isinstance(value, bool): return sqlparse.sql.Token( sqlparse.tokens.Number, sqlalchemy.types.Boolean().literal_processor(self._engine.dialect)(value)) # bytes - elif type(value) is bytes: + elif isinstance(value, bytes): if self._engine.url.get_backend_name() in ["mysql", "sqlite"]: return sqlparse.sql.Token(sqlparse.tokens.Other, f"x'{value.hex()}'") # https://dev.mysql.com/doc/refman/8.0/en/hexadecimal-literals.html elif self._engine.url.get_backend_name() == "postgresql": @@ -447,37 +447,37 @@ def __escape(value): raise RuntimeError("unsupported value: {}".format(value)) # datetime.date - elif type(value) is datetime.date: + elif isinstance(value, datetime.date): return sqlparse.sql.Token( sqlparse.tokens.String, sqlalchemy.types.String().literal_processor(self._engine.dialect)(value.strftime("%Y-%m-%d"))) # datetime.datetime - elif type(value) is datetime.datetime: + elif isinstance(value, datetime.datetime): return sqlparse.sql.Token( sqlparse.tokens.String, sqlalchemy.types.String().literal_processor(self._engine.dialect)(value.strftime("%Y-%m-%d %H:%M:%S"))) # datetime.time - elif type(value) is datetime.time: + elif isinstance(value, datetime.time): return sqlparse.sql.Token( sqlparse.tokens.String, sqlalchemy.types.String().literal_processor(self._engine.dialect)(value.strftime("%H:%M:%S"))) # float - elif type(value) is float: + elif isinstance(value, float): return sqlparse.sql.Token( sqlparse.tokens.Number, sqlalchemy.types.Float().literal_processor(self._engine.dialect)(value)) # int - elif type(value) is int: + elif isinstance(value, int): return sqlparse.sql.Token( sqlparse.tokens.Number, sqlalchemy.types.Integer().literal_processor(self._engine.dialect)(value)) # str - elif type(value) is str: + elif isinstance(value, str): return sqlparse.sql.Token( sqlparse.tokens.String, sqlalchemy.types.String().literal_processor(self._engine.dialect)(value)) @@ -493,7 +493,7 @@ def __escape(value): raise RuntimeError("unsupported value: {}".format(value)) # Escape value(s), separating with commas as needed - if type(value) in [list, tuple]: + if isinstance(value, (list, tuple)): return sqlparse.sql.TokenList(sqlparse.parse(", ".join([str(__escape(v)) for v in value]))) else: return __escape(value) From 6096c7e47aa75f18d9aa1f728538e33e37b763ba Mon Sep 17 00:00:00 2001 From: up-n-atom <adam.jaremko@gmail.com> Date: Sun, 29 Jan 2023 03:00:43 -0500 Subject: [PATCH 20/44] Fix order of datetime type checks datetime.datetime inherits datetime.date and will prematurely evaluate as an instance of datetime.date. --- src/cs50/sql.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/cs50/sql.py b/src/cs50/sql.py index 4f9457b..24690e3 100644 --- a/src/cs50/sql.py +++ b/src/cs50/sql.py @@ -446,18 +446,18 @@ def __escape(value): else: raise RuntimeError("unsupported value: {}".format(value)) - # datetime.date - elif isinstance(value, datetime.date): - return sqlparse.sql.Token( - sqlparse.tokens.String, - sqlalchemy.types.String().literal_processor(self._engine.dialect)(value.strftime("%Y-%m-%d"))) - # datetime.datetime elif isinstance(value, datetime.datetime): return sqlparse.sql.Token( sqlparse.tokens.String, sqlalchemy.types.String().literal_processor(self._engine.dialect)(value.strftime("%Y-%m-%d %H:%M:%S"))) + # datetime.date + elif isinstance(value, datetime.date): + return sqlparse.sql.Token( + sqlparse.tokens.String, + sqlalchemy.types.String().literal_processor(self._engine.dialect)(value.strftime("%Y-%m-%d"))) + # datetime.time elif isinstance(value, datetime.time): return sqlparse.sql.Token( From 53cf4d204f21d6c66f1b9ba6fe2055f0e6037feb Mon Sep 17 00:00:00 2001 From: Rongxin Liu <rongxinliu.dev@gmail.com> Date: Sun, 29 Jan 2023 16:58:03 +0800 Subject: [PATCH 21/44] bumped version number --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 62a7abe..73b37a1 100644 --- a/setup.py +++ b/setup.py @@ -18,5 +18,5 @@ package_dir={"": "src"}, packages=["cs50"], url="https://github.com/cs50/python-cs50", - version="9.2.4" + version="9.2.5" ) From 464f237e32bc2f87affd34722a498054dc0d57f6 Mon Sep 17 00:00:00 2001 From: Rongxin Liu <rongxinliu.dev@gmail.com> Date: Sun, 29 Jan 2023 18:39:42 +0800 Subject: [PATCH 22/44] fixated SQLAlchemy to version 1.4.46 --- .github/workflows/main.yml | 2 +- .gitignore | 1 + setup.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f438d7b..964db68 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -28,7 +28,7 @@ jobs: - name: Setup databases run: | pip install . - pip install mysqlclient psycopg2-binary + pip install mysqlclient psycopg2-binary SQLAlchemy==1.4.46 - name: Run tests run: python tests/sql.py diff --git a/.gitignore b/.gitignore index 4286ed6..dd3ffcc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .* !/.github/ !.gitignore +build/ *.db *.egg-info/ *.pyc diff --git a/setup.py b/setup.py index 73b37a1..1a8ef3a 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ "Topic :: Software Development :: Libraries :: Python Modules" ], description="CS50 library for Python", - install_requires=["Flask>=1.0", "SQLAlchemy", "sqlparse", "termcolor", "wheel"], + install_requires=["Flask>=1.0", "SQLAlchemy==1.4.46", "sqlparse", "termcolor", "wheel"], keywords="cs50", license="GPLv3", long_description_content_type="text/markdown", From f807f1ef8b50b72d9307c0130828af41b99f517d Mon Sep 17 00:00:00 2001 From: Rongxin Liu <rongxinliu.dev@gmail.com> Date: Wed, 13 Sep 2023 11:10:54 -0400 Subject: [PATCH 23/44] added support for SQLAlchemy 2.0 --- setup.py | 4 ++-- src/cs50/sql.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index 1a8ef3a..2c25e53 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ "Topic :: Software Development :: Libraries :: Python Modules" ], description="CS50 library for Python", - install_requires=["Flask>=1.0", "SQLAlchemy==1.4.46", "sqlparse", "termcolor", "wheel"], + install_requires=["Flask>=1.0", "SQLAlchemy<3", "sqlparse", "termcolor", "wheel"], keywords="cs50", license="GPLv3", long_description_content_type="text/markdown", @@ -18,5 +18,5 @@ package_dir={"": "src"}, packages=["cs50"], url="https://github.com/cs50/python-cs50", - version="9.2.5" + version="9.2.6" ) diff --git a/src/cs50/sql.py b/src/cs50/sql.py index 24690e3..8110cba 100644 --- a/src/cs50/sql.py +++ b/src/cs50/sql.py @@ -53,7 +53,7 @@ def __init__(self, url, **kwargs): import sqlalchemy import sqlalchemy.orm import threading - + # Temporary fix for missing sqlite3 module on the buildpack stack try: import sqlite3 @@ -100,7 +100,7 @@ def connect(dbapi_connection, connection_record): self._logger.disabled = True try: connection = self._engine.connect() - connection.execute("SELECT 1") + connection.execute(sqlalchemy.text("SELECT 1")) connection.close() except sqlalchemy.exc.OperationalError as e: e = RuntimeError(_parse_exception(e)) @@ -344,7 +344,7 @@ def teardown_appcontext(exception): if command == "SELECT": # Coerce types - rows = [dict(row) for row in result.fetchall()] + rows = [dict(row) for row in result.mappings().all()] for row in rows: for column in row: From ca09441d54ab52805baa38d50fe47c33bf4dcc65 Mon Sep 17 00:00:00 2001 From: Rongxin Liu <rongxinliu.dev@gmail.com> Date: Wed, 13 Sep 2023 11:17:39 -0400 Subject: [PATCH 24/44] convert string to sqlalchemy text --- src/cs50/sql.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cs50/sql.py b/src/cs50/sql.py index 8110cba..527a98d 100644 --- a/src/cs50/sql.py +++ b/src/cs50/sql.py @@ -370,7 +370,7 @@ def teardown_appcontext(exception): # "(psycopg2.errors.ObjectNotInPrerequisiteState) lastval is not yet defined in this session", # a la https://stackoverflow.com/a/24186770/5156190; # cf. https://www.psycopg.org/docs/errors.html re 55000 - result = connection.execute(""" + result = connection.execute(sqlalchemy.text(""" CREATE OR REPLACE FUNCTION _LASTVAL() RETURNS integer LANGUAGE plpgsql AS $$ @@ -382,7 +382,7 @@ def teardown_appcontext(exception): END; END $$; SELECT _LASTVAL(); - """) + """)) ret = result.first()[0] # If not PostgreSQL From 2ed4803dbe6f29f0b10ddb1ef5b173d991d43805 Mon Sep 17 00:00:00 2001 From: Rongxin Liu <rongxinliu.dev@gmail.com> Date: Thu, 14 Sep 2023 23:28:20 -0400 Subject: [PATCH 25/44] bump SQLAlchemy version to 1.4.49 in workflow --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 964db68..6547cf2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -28,7 +28,7 @@ jobs: - name: Setup databases run: | pip install . - pip install mysqlclient psycopg2-binary SQLAlchemy==1.4.46 + pip install mysqlclient psycopg2-binary SQLAlchemy==1.4.49 - name: Run tests run: python tests/sql.py From b8581fe410f95174edd555e428f32af8acb7bfd7 Mon Sep 17 00:00:00 2001 From: Aivar Annamaa <aivarannamaa@users.noreply.github.com> Date: Sat, 23 Sep 2023 12:55:48 +0300 Subject: [PATCH 26/44] Fix method delegation in _flushfile Required when the faked stream is already faked and the original fake also uses method delegation. --- src/cs50/cs50.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cs50/cs50.py b/src/cs50/cs50.py index 16bfd0b..425173c 100644 --- a/src/cs50/cs50.py +++ b/src/cs50/cs50.py @@ -49,7 +49,7 @@ def __init__(self, f): self.f = f def __getattr__(self, name): - return object.__getattribute__(self.f, name) + return getattr(self.f, name) def write(self, x): self.f.write(x) From 4424e657431475114fc46cb86889ac08d2ce62a9 Mon Sep 17 00:00:00 2001 From: Rongxin Liu <rongxinliu.dev@gmail.com> Date: Fri, 29 Sep 2023 16:07:48 -0400 Subject: [PATCH 27/44] added support for 'CREATE VIEW' statement --- setup.py | 2 +- src/cs50/sql.py | 24 ++++++++++++++---------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/setup.py b/setup.py index 1a8ef3a..bd33a73 100644 --- a/setup.py +++ b/setup.py @@ -18,5 +18,5 @@ package_dir={"": "src"}, packages=["cs50"], url="https://github.com/cs50/python-cs50", - version="9.2.5" + version="9.2.6" ) diff --git a/src/cs50/sql.py b/src/cs50/sql.py index 24690e3..8d07327 100644 --- a/src/cs50/sql.py +++ b/src/cs50/sql.py @@ -53,7 +53,7 @@ def __init__(self, url, **kwargs): import sqlalchemy import sqlalchemy.orm import threading - + # Temporary fix for missing sqlite3 module on the buildpack stack try: import sqlite3 @@ -149,15 +149,15 @@ def execute(self, sql, *args, **kwargs): if len(args) > 0 and len(kwargs) > 0: raise RuntimeError("cannot pass both positional and named parameters") - # Infer command from (unflattened) statement - for token in statements[0]: - if token.ttype in [sqlparse.tokens.Keyword, sqlparse.tokens.Keyword.DDL, sqlparse.tokens.Keyword.DML]: - token_value = token.value.upper() - if token_value in ["BEGIN", "DELETE", "INSERT", "SELECT", "START", "UPDATE"]: - command = token_value - break - else: - command = None + # Infer command from flattened statement to a single string separated by spaces + full_statement = ' '.join(str(token) for token in statements[0].tokens if token.ttype in [sqlparse.tokens.Keyword, sqlparse.tokens.Keyword.DDL, sqlparse.tokens.Keyword.DML]) + full_statement = full_statement.upper() + + # set of possible commands + commands = {"BEGIN", "CREATE VIEW", "DELETE", "INSERT", "SELECT", "START", "UPDATE"} + + # check if the full_statement starts with any command + command = next((cmd for cmd in commands if full_statement.startswith(cmd)), None) # Flatten statement tokens = list(statements[0].flatten()) @@ -393,6 +393,10 @@ def teardown_appcontext(exception): elif command in ["DELETE", "UPDATE"]: ret = result.rowcount + # If CREATE VIEW, return True + elif command == "CREATE VIEW": + ret = True + # If constraint violated except sqlalchemy.exc.IntegrityError as e: self._logger.error(termcolor.colored(_statement, "red")) From 0608340a51d5fc60742b4a94ab3f9b45e7d45f2a Mon Sep 17 00:00:00 2001 From: Rongxin Liu <rongxinliu.dev@gmail.com> Date: Wed, 4 Oct 2023 11:29:20 -0700 Subject: [PATCH 28/44] unpin SQLAlchemy version in workflow --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6547cf2..b8165f7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -28,7 +28,7 @@ jobs: - name: Setup databases run: | pip install . - pip install mysqlclient psycopg2-binary SQLAlchemy==1.4.49 + pip install mysqlclient psycopg2-binary SQLAlchemy - name: Run tests run: python tests/sql.py From b3f0a0c5d0d5324595791f02bc3faae49cf1f15d Mon Sep 17 00:00:00 2001 From: Rongxin Liu <rongxinliu.dev@gmail.com> Date: Tue, 17 Oct 2023 16:00:07 -0400 Subject: [PATCH 29/44] replaced distutils with packaging --- setup.py | 4 ++-- src/cs50/cs50.py | 1 - src/cs50/flask.py | 7 +++++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index bd33a73..d9a2ee4 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ "Topic :: Software Development :: Libraries :: Python Modules" ], description="CS50 library for Python", - install_requires=["Flask>=1.0", "SQLAlchemy==1.4.46", "sqlparse", "termcolor", "wheel"], + install_requires=["Flask>=1.0", "packaging", "SQLAlchemy==1.4.46", "sqlparse", "termcolor", "wheel"], keywords="cs50", license="GPLv3", long_description_content_type="text/markdown", @@ -18,5 +18,5 @@ package_dir={"": "src"}, packages=["cs50"], url="https://github.com/cs50/python-cs50", - version="9.2.6" + version="9.2.7" ) diff --git a/src/cs50/cs50.py b/src/cs50/cs50.py index 16bfd0b..31313f8 100644 --- a/src/cs50/cs50.py +++ b/src/cs50/cs50.py @@ -6,7 +6,6 @@ import re import sys -from distutils.sysconfig import get_python_lib from os.path import abspath, join from termcolor import colored from traceback import format_exception diff --git a/src/cs50/flask.py b/src/cs50/flask.py index 324ec30..3668007 100644 --- a/src/cs50/flask.py +++ b/src/cs50/flask.py @@ -6,10 +6,13 @@ def _wrap_flask(f): if f is None: return - from distutils.version import StrictVersion + from packaging.version import Version, InvalidVersion from .cs50 import _formatException - if f.__version__ < StrictVersion("1.0"): + try: + if Version(f.__version__) < Version("1.0"): + return + except InvalidVersion: return if os.getenv("CS50_IDE_TYPE") == "online": From bb7263454280a73077ee1657956d030f6fd60f31 Mon Sep 17 00:00:00 2001 From: Rongxin Liu <rongxinliu.dev@gmail.com> Date: Tue, 24 Oct 2023 09:36:58 -0400 Subject: [PATCH 30/44] bumped version number, fixed comment styles --- setup.py | 2 +- src/cs50/sql.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index d9a2ee4..8c1abfb 100644 --- a/setup.py +++ b/setup.py @@ -18,5 +18,5 @@ package_dir={"": "src"}, packages=["cs50"], url="https://github.com/cs50/python-cs50", - version="9.2.7" + version="9.3.0" ) diff --git a/src/cs50/sql.py b/src/cs50/sql.py index fee0d68..de3ad56 100644 --- a/src/cs50/sql.py +++ b/src/cs50/sql.py @@ -153,10 +153,10 @@ def execute(self, sql, *args, **kwargs): full_statement = ' '.join(str(token) for token in statements[0].tokens if token.ttype in [sqlparse.tokens.Keyword, sqlparse.tokens.Keyword.DDL, sqlparse.tokens.Keyword.DML]) full_statement = full_statement.upper() - # set of possible commands + # Set of possible commands commands = {"BEGIN", "CREATE VIEW", "DELETE", "INSERT", "SELECT", "START", "UPDATE"} - # check if the full_statement starts with any command + # Check if the full_statement starts with any command command = next((cmd for cmd in commands if full_statement.startswith(cmd)), None) # Flatten statement From 3ddef3132a2485d48ea487bb9401317921ebfc82 Mon Sep 17 00:00:00 2001 From: Rongxin Liu <rongxinliu.dev@gmail.com> Date: Tue, 24 Oct 2023 09:39:36 -0400 Subject: [PATCH 31/44] updated SQLAlchemy version constraint --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8c1abfb..23f6b01 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ "Topic :: Software Development :: Libraries :: Python Modules" ], description="CS50 library for Python", - install_requires=["Flask>=1.0", "packaging", "SQLAlchemy==1.4.46", "sqlparse", "termcolor", "wheel"], + install_requires=["Flask>=1.0", "packaging", "SQLAlchemy<3", "sqlparse", "termcolor", "wheel"], keywords="cs50", license="GPLv3", long_description_content_type="text/markdown", From 1b644dd0c4a4fd252306933ed2a1247ffda8de83 Mon Sep 17 00:00:00 2001 From: "David J. Malan" <malan@harvard.edu> Date: Sun, 17 Dec 2023 14:20:44 -0500 Subject: [PATCH 32/44] updated IO wrapper, style, version --- setup.py | 2 +- src/cs50/__init__.py | 1 + src/cs50/cs50.py | 49 +++++++---- src/cs50/flask.py | 10 ++- src/cs50/sql.py | 199 ++++++++++++++++++++++++++++++------------- 5 files changed, 184 insertions(+), 77 deletions(-) diff --git a/setup.py b/setup.py index 23f6b01..10ceb30 100644 --- a/setup.py +++ b/setup.py @@ -18,5 +18,5 @@ package_dir={"": "src"}, packages=["cs50"], url="https://github.com/cs50/python-cs50", - version="9.3.0" + version="9.3.1" ) diff --git a/src/cs50/__init__.py b/src/cs50/__init__.py index aaec161..7dd4e17 100644 --- a/src/cs50/__init__.py +++ b/src/cs50/__init__.py @@ -8,6 +8,7 @@ # Import cs50_* from .cs50 import get_char, get_float, get_int, get_string + try: from .cs50 import get_long except ImportError: diff --git a/src/cs50/cs50.py b/src/cs50/cs50.py index 07f13e9..f331a88 100644 --- a/src/cs50/cs50.py +++ b/src/cs50/cs50.py @@ -17,7 +17,9 @@ try: # Patch formatException - logging.root.handlers[0].formatter.formatException = lambda exc_info: _formatException(*exc_info) + logging.root.handlers[ + 0 + ].formatter.formatException = lambda exc_info: _formatException(*exc_info) except IndexError: pass @@ -37,26 +39,31 @@ _logger.addHandler(handler) -class _flushfile(): +class _Unbuffered: """ Disable buffering for standard output and standard error. - http://stackoverflow.com/a/231216 + https://stackoverflow.com/a/107717 + https://docs.python.org/3/library/io.html """ - def __init__(self, f): - self.f = f + def __init__(self, stream): + self.stream = stream - def __getattr__(self, name): - return getattr(self.f, name) + def __getattr__(self, attr): + return getattr(self.stream, attr) - def write(self, x): - self.f.write(x) - self.f.flush() + def write(self, b): + self.stream.write(b) + self.stream.flush() + def writelines(self, lines): + self.stream.writelines(lines) + self.stream.flush() -sys.stderr = _flushfile(sys.stderr) -sys.stdout = _flushfile(sys.stdout) + +sys.stderr = _Unbuffered(sys.stderr) +sys.stdout = _Unbuffered(sys.stdout) def _formatException(type, value, tb): @@ -78,19 +85,29 @@ def _formatException(type, value, tb): lines += line else: matches = re.search(r"^(\s*)(.*?)(\s*)$", line, re.DOTALL) - lines.append(matches.group(1) + colored(matches.group(2), "yellow") + matches.group(3)) + lines.append( + matches.group(1) + + colored(matches.group(2), "yellow") + + matches.group(3) + ) return "".join(lines).rstrip() -sys.excepthook = lambda type, value, tb: print(_formatException(type, value, tb), file=sys.stderr) +sys.excepthook = lambda type, value, tb: print( + _formatException(type, value, tb), file=sys.stderr +) def eprint(*args, **kwargs): - raise RuntimeError("The CS50 Library for Python no longer supports eprint, but you can use print instead!") + raise RuntimeError( + "The CS50 Library for Python no longer supports eprint, but you can use print instead!" + ) def get_char(prompt): - raise RuntimeError("The CS50 Library for Python no longer supports get_char, but you can use get_string instead!") + raise RuntimeError( + "The CS50 Library for Python no longer supports get_char, but you can use get_string instead!" + ) def get_float(prompt): diff --git a/src/cs50/flask.py b/src/cs50/flask.py index 3668007..6e38971 100644 --- a/src/cs50/flask.py +++ b/src/cs50/flask.py @@ -2,6 +2,7 @@ import pkgutil import sys + def _wrap_flask(f): if f is None: return @@ -17,10 +18,15 @@ def _wrap_flask(f): if os.getenv("CS50_IDE_TYPE") == "online": from werkzeug.middleware.proxy_fix import ProxyFix + _flask_init_before = f.Flask.__init__ + def _flask_init_after(self, *args, **kwargs): _flask_init_before(self, *args, **kwargs) - self.wsgi_app = ProxyFix(self.wsgi_app, x_proto=1) # For HTTPS-to-HTTP proxy + self.wsgi_app = ProxyFix( + self.wsgi_app, x_proto=1 + ) # For HTTPS-to-HTTP proxy + f.Flask.__init__ = _flask_init_after @@ -30,7 +36,7 @@ def _flask_init_after(self, *args, **kwargs): # If Flask wasn't imported else: - flask_loader = pkgutil.get_loader('flask') + flask_loader = pkgutil.get_loader("flask") if flask_loader: _exec_module_before = flask_loader.exec_module diff --git a/src/cs50/sql.py b/src/cs50/sql.py index de3ad56..a0b93eb 100644 --- a/src/cs50/sql.py +++ b/src/cs50/sql.py @@ -14,7 +14,6 @@ def _enable_logging(f): @functools.wraps(f) def decorator(*args, **kwargs): - # Infer whether Flask is installed try: import flask @@ -71,17 +70,20 @@ def __init__(self, url, **kwargs): # Create engine, disabling SQLAlchemy's own autocommit mode raising exception if back end's module not installed; # without isolation_level, PostgreSQL warns with "there is already a transaction in progress" for our own BEGIN and # "there is no transaction in progress" for our own COMMIT - self._engine = sqlalchemy.create_engine(url, **kwargs).execution_options(autocommit=False, isolation_level="AUTOCOMMIT") + self._engine = sqlalchemy.create_engine(url, **kwargs).execution_options( + autocommit=False, isolation_level="AUTOCOMMIT" + ) # Get logger self._logger = logging.getLogger("cs50") # Listener for connections def connect(dbapi_connection, connection_record): - # Enable foreign key constraints try: - if isinstance(dbapi_connection, sqlite3.Connection): # If back end is sqlite + if isinstance( + dbapi_connection, sqlite3.Connection + ): # If back end is sqlite cursor = dbapi_connection.cursor() cursor.execute("PRAGMA foreign_keys=ON") cursor.close() @@ -150,14 +152,33 @@ def execute(self, sql, *args, **kwargs): raise RuntimeError("cannot pass both positional and named parameters") # Infer command from flattened statement to a single string separated by spaces - full_statement = ' '.join(str(token) for token in statements[0].tokens if token.ttype in [sqlparse.tokens.Keyword, sqlparse.tokens.Keyword.DDL, sqlparse.tokens.Keyword.DML]) + full_statement = " ".join( + str(token) + for token in statements[0].tokens + if token.ttype + in [ + sqlparse.tokens.Keyword, + sqlparse.tokens.Keyword.DDL, + sqlparse.tokens.Keyword.DML, + ] + ) full_statement = full_statement.upper() # Set of possible commands - commands = {"BEGIN", "CREATE VIEW", "DELETE", "INSERT", "SELECT", "START", "UPDATE"} + commands = { + "BEGIN", + "CREATE VIEW", + "DELETE", + "INSERT", + "SELECT", + "START", + "UPDATE", + } # Check if the full_statement starts with any command - command = next((cmd for cmd in commands if full_statement.startswith(cmd)), None) + command = next( + (cmd for cmd in commands if full_statement.startswith(cmd)), None + ) # Flatten statement tokens = list(statements[0].flatten()) @@ -166,10 +187,8 @@ def execute(self, sql, *args, **kwargs): placeholders = {} paramstyle = None for index, token in enumerate(tokens): - # If token is a placeholder if token.ttype == sqlparse.tokens.Name.Placeholder: - # Determine paramstyle, name _paramstyle, name = _parse_placeholder(token) @@ -186,7 +205,6 @@ def execute(self, sql, *args, **kwargs): # If no placeholders if not paramstyle: - # Error-check like qmark if args if args: paramstyle = "qmark" @@ -201,13 +219,20 @@ def execute(self, sql, *args, **kwargs): # qmark if paramstyle == "qmark": - # Validate number of placeholders if len(placeholders) != len(args): if len(placeholders) < len(args): - raise RuntimeError("fewer placeholders ({}) than values ({})".format(_placeholders, _args)) + raise RuntimeError( + "fewer placeholders ({}) than values ({})".format( + _placeholders, _args + ) + ) else: - raise RuntimeError("more placeholders ({}) than values ({})".format(_placeholders, _args)) + raise RuntimeError( + "more placeholders ({}) than values ({})".format( + _placeholders, _args + ) + ) # Escape values for i, index in enumerate(placeholders.keys()): @@ -215,27 +240,34 @@ def execute(self, sql, *args, **kwargs): # numeric elif paramstyle == "numeric": - # Escape values for index, i in placeholders.items(): if i >= len(args): - raise RuntimeError("missing value for placeholder (:{})".format(i + 1, len(args))) + raise RuntimeError( + "missing value for placeholder (:{})".format(i + 1, len(args)) + ) tokens[index] = self._escape(args[i]) # Check if any values unused indices = set(range(len(args))) - set(placeholders.values()) if indices: - raise RuntimeError("unused {} ({})".format( - "value" if len(indices) == 1 else "values", - ", ".join([str(self._escape(args[index])) for index in indices]))) + raise RuntimeError( + "unused {} ({})".format( + "value" if len(indices) == 1 else "values", + ", ".join( + [str(self._escape(args[index])) for index in indices] + ), + ) + ) # named elif paramstyle == "named": - # Escape values for index, name in placeholders.items(): if name not in kwargs: - raise RuntimeError("missing value for placeholder (:{})".format(name)) + raise RuntimeError( + "missing value for placeholder (:{})".format(name) + ) tokens[index] = self._escape(kwargs[name]) # Check if any keys unused @@ -245,13 +277,20 @@ def execute(self, sql, *args, **kwargs): # format elif paramstyle == "format": - # Validate number of placeholders if len(placeholders) != len(args): if len(placeholders) < len(args): - raise RuntimeError("fewer placeholders ({}) than values ({})".format(_placeholders, _args)) + raise RuntimeError( + "fewer placeholders ({}) than values ({})".format( + _placeholders, _args + ) + ) else: - raise RuntimeError("more placeholders ({}) than values ({})".format(_placeholders, _args)) + raise RuntimeError( + "more placeholders ({}) than values ({})".format( + _placeholders, _args + ) + ) # Escape values for i, index in enumerate(placeholders.keys()): @@ -259,40 +298,44 @@ def execute(self, sql, *args, **kwargs): # pyformat elif paramstyle == "pyformat": - # Escape values for index, name in placeholders.items(): if name not in kwargs: - raise RuntimeError("missing value for placeholder (%{}s)".format(name)) + raise RuntimeError( + "missing value for placeholder (%{}s)".format(name) + ) tokens[index] = self._escape(kwargs[name]) # Check if any keys unused keys = kwargs.keys() - placeholders.values() if keys: - raise RuntimeError("unused {} ({})".format( - "value" if len(keys) == 1 else "values", - ", ".join(keys))) + raise RuntimeError( + "unused {} ({})".format( + "value" if len(keys) == 1 else "values", ", ".join(keys) + ) + ) # For SQL statements where a colon is required verbatim, as within an inline string, use a backslash to escape # https://docs.sqlalchemy.org/en/13/core/sqlelement.html?highlight=text#sqlalchemy.sql.expression.text for index, token in enumerate(tokens): - # In string literal # https://www.sqlite.org/lang_keywords.html - if token.ttype in [sqlparse.tokens.Literal.String, sqlparse.tokens.Literal.String.Single]: + if token.ttype in [ + sqlparse.tokens.Literal.String, + sqlparse.tokens.Literal.String.Single, + ]: token.value = re.sub("(^'|\s+):", r"\1\:", token.value) # In identifier # https://www.sqlite.org/lang_keywords.html elif token.ttype == sqlparse.tokens.Literal.String.Symbol: - token.value = re.sub("(^\"|\s+):", r"\1\:", token.value) + token.value = re.sub('(^"|\s+):', r"\1\:", token.value) # Join tokens into statement statement = "".join([str(token) for token in tokens]) # If no connection yet if not hasattr(_data, self._name()): - # Connect to database setattr(_data, self._name(), self._engine.connect()) @@ -302,9 +345,12 @@ def execute(self, sql, *args, **kwargs): # Disconnect if/when a Flask app is torn down try: import flask + assert flask.current_app + def teardown_appcontext(exception): self._disconnect() + if teardown_appcontext not in flask.current_app.teardown_appcontext_funcs: flask.current_app.teardown_appcontext(teardown_appcontext) except (ModuleNotFoundError, AssertionError): @@ -312,15 +358,20 @@ def teardown_appcontext(exception): # Catch SQLAlchemy warnings with warnings.catch_warnings(): - # Raise exceptions for warnings warnings.simplefilter("error") # Prepare, execute statement try: - # Join tokens into statement, abbreviating binary data as <class 'bytes'> - _statement = "".join([str(bytes) if token.ttype == sqlparse.tokens.Other else str(token) for token in tokens]) + _statement = "".join( + [ + str(bytes) + if token.ttype == sqlparse.tokens.Other + else str(token) + for token in tokens + ] + ) # Check for start of transaction if command in ["BEGIN", "START"]: @@ -342,12 +393,10 @@ def teardown_appcontext(exception): # If SELECT, return result set as list of dict objects if command == "SELECT": - # Coerce types rows = [dict(row) for row in result.mappings().all()] for row in rows: for column in row: - # Coerce decimal.Decimal objects to float objects # https://groups.google.com/d/msg/sqlalchemy/0qXMYJvq8SA/oqtvMD9Uw-kJ if isinstance(row[column], decimal.Decimal): @@ -362,15 +411,15 @@ def teardown_appcontext(exception): # If INSERT, return primary key value for a newly inserted row (or None if none) elif command == "INSERT": - # If PostgreSQL if self._engine.url.get_backend_name() == "postgresql": - # Return LASTVAL() or NULL, avoiding # "(psycopg2.errors.ObjectNotInPrerequisiteState) lastval is not yet defined in this session", # a la https://stackoverflow.com/a/24186770/5156190; # cf. https://www.psycopg.org/docs/errors.html re 55000 - result = connection.execute(sqlalchemy.text(""" + result = connection.execute( + sqlalchemy.text( + """ CREATE OR REPLACE FUNCTION _LASTVAL() RETURNS integer LANGUAGE plpgsql AS $$ @@ -382,7 +431,9 @@ def teardown_appcontext(exception): END; END $$; SELECT _LASTVAL(); - """)) + """ + ) + ) ret = result.first()[0] # If not PostgreSQL @@ -405,7 +456,10 @@ def teardown_appcontext(exception): raise e # If user error - except (sqlalchemy.exc.OperationalError, sqlalchemy.exc.ProgrammingError) as e: + except ( + sqlalchemy.exc.OperationalError, + sqlalchemy.exc.ProgrammingError, + ) as e: self._disconnect() self._logger.error(termcolor.colored(_statement, "red")) e = RuntimeError(e.orig) @@ -430,7 +484,6 @@ def _escape(self, value): import sqlparse def __escape(value): - # Lazily import import datetime import sqlalchemy @@ -439,14 +492,21 @@ def __escape(value): if isinstance(value, bool): return sqlparse.sql.Token( sqlparse.tokens.Number, - sqlalchemy.types.Boolean().literal_processor(self._engine.dialect)(value)) + sqlalchemy.types.Boolean().literal_processor(self._engine.dialect)( + value + ), + ) # bytes elif isinstance(value, bytes): if self._engine.url.get_backend_name() in ["mysql", "sqlite"]: - return sqlparse.sql.Token(sqlparse.tokens.Other, f"x'{value.hex()}'") # https://dev.mysql.com/doc/refman/8.0/en/hexadecimal-literals.html + return sqlparse.sql.Token( + sqlparse.tokens.Other, f"x'{value.hex()}'" + ) # https://dev.mysql.com/doc/refman/8.0/en/hexadecimal-literals.html elif self._engine.url.get_backend_name() == "postgresql": - return sqlparse.sql.Token(sqlparse.tokens.Other, f"'\\x{value.hex()}'") # https://dba.stackexchange.com/a/203359 + return sqlparse.sql.Token( + sqlparse.tokens.Other, f"'\\x{value.hex()}'" + ) # https://dba.stackexchange.com/a/203359 else: raise RuntimeError("unsupported value: {}".format(value)) @@ -454,43 +514,59 @@ def __escape(value): elif isinstance(value, datetime.datetime): return sqlparse.sql.Token( sqlparse.tokens.String, - sqlalchemy.types.String().literal_processor(self._engine.dialect)(value.strftime("%Y-%m-%d %H:%M:%S"))) + sqlalchemy.types.String().literal_processor(self._engine.dialect)( + value.strftime("%Y-%m-%d %H:%M:%S") + ), + ) # datetime.date elif isinstance(value, datetime.date): return sqlparse.sql.Token( sqlparse.tokens.String, - sqlalchemy.types.String().literal_processor(self._engine.dialect)(value.strftime("%Y-%m-%d"))) + sqlalchemy.types.String().literal_processor(self._engine.dialect)( + value.strftime("%Y-%m-%d") + ), + ) # datetime.time elif isinstance(value, datetime.time): return sqlparse.sql.Token( sqlparse.tokens.String, - sqlalchemy.types.String().literal_processor(self._engine.dialect)(value.strftime("%H:%M:%S"))) + sqlalchemy.types.String().literal_processor(self._engine.dialect)( + value.strftime("%H:%M:%S") + ), + ) # float elif isinstance(value, float): return sqlparse.sql.Token( sqlparse.tokens.Number, - sqlalchemy.types.Float().literal_processor(self._engine.dialect)(value)) + sqlalchemy.types.Float().literal_processor(self._engine.dialect)( + value + ), + ) # int elif isinstance(value, int): return sqlparse.sql.Token( sqlparse.tokens.Number, - sqlalchemy.types.Integer().literal_processor(self._engine.dialect)(value)) + sqlalchemy.types.Integer().literal_processor(self._engine.dialect)( + value + ), + ) # str elif isinstance(value, str): return sqlparse.sql.Token( sqlparse.tokens.String, - sqlalchemy.types.String().literal_processor(self._engine.dialect)(value)) + sqlalchemy.types.String().literal_processor(self._engine.dialect)( + value + ), + ) # None elif value is None: - return sqlparse.sql.Token( - sqlparse.tokens.Keyword, - sqlalchemy.null()) + return sqlparse.sql.Token(sqlparse.tokens.Keyword, sqlalchemy.null()) # Unsupported value else: @@ -498,7 +574,9 @@ def __escape(value): # Escape value(s), separating with commas as needed if isinstance(value, (list, tuple)): - return sqlparse.sql.TokenList(sqlparse.parse(", ".join([str(__escape(v)) for v in value]))) + return sqlparse.sql.TokenList( + sqlparse.parse(", ".join([str(__escape(v)) for v in value])) + ) else: return __escape(value) @@ -510,7 +588,9 @@ def _parse_exception(e): import re # MySQL - matches = re.search(r"^\(_mysql_exceptions\.OperationalError\) \(\d+, \"(.+)\"\)$", str(e)) + matches = re.search( + r"^\(_mysql_exceptions\.OperationalError\) \(\d+, \"(.+)\"\)$", str(e) + ) if matches: return matches.group(1) @@ -536,7 +616,10 @@ def _parse_placeholder(token): import sqlparse # Validate token - if not isinstance(token, sqlparse.sql.Token) or token.ttype != sqlparse.tokens.Name.Placeholder: + if ( + not isinstance(token, sqlparse.sql.Token) + or token.ttype != sqlparse.tokens.Name.Placeholder + ): raise TypeError() # qmark From e18486cc36f879c83eefc74a408ba411eeb35060 Mon Sep 17 00:00:00 2001 From: "David J. Malan" <malan@harvard.edu> Date: Sun, 17 Dec 2023 14:40:13 -0500 Subject: [PATCH 33/44] fixes #178 --- src/cs50/sql.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/cs50/sql.py b/src/cs50/sql.py index de3ad56..ef011da 100644 --- a/src/cs50/sql.py +++ b/src/cs50/sql.py @@ -399,6 +399,8 @@ def teardown_appcontext(exception): # If constraint violated except sqlalchemy.exc.IntegrityError as e: + if self._autocommit: + connection.execute(sqlalchemy.text("ROLLBACK")) self._logger.error(termcolor.colored(_statement, "red")) e = ValueError(e.orig) e.__cause__ = None From 781c1c201090493a65793f586174e14644163f36 Mon Sep 17 00:00:00 2001 From: "David J. Malan" <malan@harvard.edu> Date: Sun, 17 Dec 2023 14:40:26 -0500 Subject: [PATCH 34/44] increments version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 23f6b01..10ceb30 100644 --- a/setup.py +++ b/setup.py @@ -18,5 +18,5 @@ package_dir={"": "src"}, packages=["cs50"], url="https://github.com/cs50/python-cs50", - version="9.3.0" + version="9.3.1" ) From 14a9741b904a437af015e0aa9d14d8531bc18de2 Mon Sep 17 00:00:00 2001 From: "David J. Malan" <malan@harvard.edu> Date: Sun, 17 Dec 2023 14:40:45 -0500 Subject: [PATCH 35/44] increments version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 10ceb30..8f4b1be 100644 --- a/setup.py +++ b/setup.py @@ -18,5 +18,5 @@ package_dir={"": "src"}, packages=["cs50"], url="https://github.com/cs50/python-cs50", - version="9.3.1" + version="9.3.2" ) From fc6647adbf1bb67cbae8e29e01095d550ed04d2e Mon Sep 17 00:00:00 2001 From: "David J. Malan" <malan@harvard.edu> Date: Sun, 17 Dec 2023 19:16:30 -0500 Subject: [PATCH 36/44] updated style --- setup.py | 2 +- src/cs50/sql.py | 13 ++++++++++++- tests/sql.py | 6 ++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 8f4b1be..7976109 100644 --- a/setup.py +++ b/setup.py @@ -18,5 +18,5 @@ package_dir={"": "src"}, packages=["cs50"], url="https://github.com/cs50/python-cs50", - version="9.3.2" + version="9.3.3" ) diff --git a/src/cs50/sql.py b/src/cs50/sql.py index 3be30fe..356ed62 100644 --- a/src/cs50/sql.py +++ b/src/cs50/sql.py @@ -71,9 +71,13 @@ def __init__(self, url, **kwargs): # without isolation_level, PostgreSQL warns with "there is already a transaction in progress" for our own BEGIN and # "there is no transaction in progress" for our own COMMIT self._engine = sqlalchemy.create_engine(url, **kwargs).execution_options( - autocommit=False, isolation_level="AUTOCOMMIT" + autocommit=False, isolation_level="AUTOCOMMIT", no_parameters=True ) + # Avoid doubly escaping percent signs, since no_parameters=True anyway + # https://github.com/cs50/python-cs50/issues/171 + self._engine.dialect.identifier_preparer._double_percents = False + # Get logger self._logger = logging.getLogger("cs50") @@ -559,12 +563,19 @@ def __escape(value): # str elif isinstance(value, str): +<<<<<<< HEAD return sqlparse.sql.Token( sqlparse.tokens.String, sqlalchemy.types.String().literal_processor(self._engine.dialect)( value ), ) +======= + literal = sqlalchemy.types.String().literal_processor(self._engine.dialect)(value) + #if self._engine.dialect.identifier_preparer._double_percents: + # literal = literal.replace("%%", "%") + return sqlparse.sql.Token(sqlparse.tokens.String, literal) +>>>>>>> 3863555 (fixes #171) # None elif value is None: diff --git a/tests/sql.py b/tests/sql.py index 968f98b..b5d5406 100644 --- a/tests/sql.py +++ b/tests/sql.py @@ -138,6 +138,12 @@ def test_lastrowid(self): self.assertEqual(self.db.execute("INSERT INTO foo (firstname, lastname) VALUES('firstname', 'lastname')"), 1) self.assertRaises(ValueError, self.db.execute, "INSERT INTO foo (id, firstname, lastname) VALUES(1, 'firstname', 'lastname')") + def test_url(self): + url = "https://www.amazon.es/Desesperaci%C3%B3n-BEST-SELLER-Stephen-King/dp/8497595890" + self.db.execute("CREATE TABLE foo(id SERIAL PRIMARY KEY, url TEXT)") + self.db.execute("INSERT INTO foo (url) VALUES(?)", url) + self.assertEqual(self.db.execute("SELECT url FROM foo")[0]["url"], url) + def tearDown(self): self.db.execute("DROP TABLE cs50") self.db.execute("DROP TABLE IF EXISTS foo") From 128f498e8e34f2db10c06633b2f7d2b742ecc2c9 Mon Sep 17 00:00:00 2001 From: "David J. Malan" <malan@harvard.edu> Date: Sun, 17 Dec 2023 19:15:37 -0500 Subject: [PATCH 37/44] fixed YAML --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index f795750..91f5a7d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,7 @@ services: - postgres environment: MYSQL_HOST: mysql - POSTGRESQL_HOST: postgresql + POSTGRESQL_HOST: postgres links: - mysql - postgres From 3b9036933da29b0265f7ddae0d709ed2784cfb1d Mon Sep 17 00:00:00 2001 From: "David J. Malan" <malan@harvard.edu> Date: Sun, 17 Dec 2023 19:18:14 -0500 Subject: [PATCH 38/44] fixed merge --- src/cs50/sql.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/cs50/sql.py b/src/cs50/sql.py index 356ed62..cbbd3d2 100644 --- a/src/cs50/sql.py +++ b/src/cs50/sql.py @@ -563,19 +563,12 @@ def __escape(value): # str elif isinstance(value, str): -<<<<<<< HEAD return sqlparse.sql.Token( sqlparse.tokens.String, sqlalchemy.types.String().literal_processor(self._engine.dialect)( value ), ) -======= - literal = sqlalchemy.types.String().literal_processor(self._engine.dialect)(value) - #if self._engine.dialect.identifier_preparer._double_percents: - # literal = literal.replace("%%", "%") - return sqlparse.sql.Token(sqlparse.tokens.String, literal) ->>>>>>> 3863555 (fixes #171) # None elif value is None: From e47cc3563cb015159ab897dd8053af2f887f7907 Mon Sep 17 00:00:00 2001 From: "David J. Malan" <malan@harvard.edu> Date: Sun, 17 Dec 2023 19:39:15 -0500 Subject: [PATCH 39/44] updated for MySQL tests --- docker-compose.yml | 2 +- tests/sql.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 91f5a7d..8608080 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,7 +20,7 @@ services: MYSQL_ALLOW_EMPTY_PASSWORD: yes healthcheck: test: ["CMD", "mysqladmin", "-uroot", "ping"] - image: cs50/mysql:8 + image: cs50/mysql ports: - 3306:3306 postgres: diff --git a/tests/sql.py b/tests/sql.py index b5d5406..bb37fd9 100644 --- a/tests/sql.py +++ b/tests/sql.py @@ -336,7 +336,7 @@ def test_cte(self): if __name__ == "__main__": suite = unittest.TestSuite([ unittest.TestLoader().loadTestsFromTestCase(SQLiteTests), - #unittest.TestLoader().loadTestsFromTestCase(MySQLTests), + unittest.TestLoader().loadTestsFromTestCase(MySQLTests), unittest.TestLoader().loadTestsFromTestCase(PostgresTests) ]) From dc6a43f5f93cd22457873ee858a60cbac46237f6 Mon Sep 17 00:00:00 2001 From: "David J. Malan" <malan@harvard.edu> Date: Sun, 17 Dec 2023 23:04:50 -0500 Subject: [PATCH 40/44] updated SQLAlchemy versioning --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 7976109..6fd6cfa 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ "Topic :: Software Development :: Libraries :: Python Modules" ], description="CS50 library for Python", - install_requires=["Flask>=1.0", "packaging", "SQLAlchemy<3", "sqlparse", "termcolor", "wheel"], + install_requires=["Flask>=1.0", "packaging", "SQLAlchemy>=2,<3", "sqlparse", "termcolor", "wheel"], keywords="cs50", license="GPLv3", long_description_content_type="text/markdown", @@ -18,5 +18,5 @@ package_dir={"": "src"}, packages=["cs50"], url="https://github.com/cs50/python-cs50", - version="9.3.3" + version="9.3.4" ) From 688af684d61a01e3bec5008acf2293a6d505196f Mon Sep 17 00:00:00 2001 From: Rongxin Liu <rongxinliu.dev@gmail.com> Date: Tue, 5 Mar 2024 00:23:11 +0700 Subject: [PATCH 41/44] updated workflow actions --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b8165f7..0f14704 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -20,8 +20,8 @@ jobs: ports: - 5432:5432 steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: python-version: '3.7' check-latest: true From d67a26b3891a8d093aaa3a53e0f9a7bfeb0a599e Mon Sep 17 00:00:00 2001 From: "yulai.linda@gmail.com" <rongxinliu.dev@gmail.com> Date: Thu, 2 May 2024 12:22:33 -0400 Subject: [PATCH 42/44] updated actions/github-script to version v7 --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0f14704..0b0ee1a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -56,7 +56,7 @@ jobs: - name: Create Release if: ${{ github.ref == 'refs/heads/main' }} - uses: actions/github-script@v6 + uses: actions/github-script@v7 with: github-token: ${{ github.token }} script: | From 2d5fd94132accb2733833eb273d755803a99794c Mon Sep 17 00:00:00 2001 From: "David J. Malan" <malan@harvard.edu> Date: Mon, 14 Oct 2024 20:39:22 -0400 Subject: [PATCH 43/44] adds support for VACUUM --- setup.py | 2 +- src/cs50/sql.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 6fd6cfa..1817b95 100644 --- a/setup.py +++ b/setup.py @@ -18,5 +18,5 @@ package_dir={"": "src"}, packages=["cs50"], url="https://github.com/cs50/python-cs50", - version="9.3.4" + version="9.4.0" ) diff --git a/src/cs50/sql.py b/src/cs50/sql.py index cbbd3d2..a133293 100644 --- a/src/cs50/sql.py +++ b/src/cs50/sql.py @@ -177,6 +177,7 @@ def execute(self, sql, *args, **kwargs): "SELECT", "START", "UPDATE", + "VACUUM", } # Check if the full_statement starts with any command @@ -378,7 +379,7 @@ def teardown_appcontext(exception): ) # Check for start of transaction - if command in ["BEGIN", "START"]: + if command in ["BEGIN", "START", "VACUUM"]: # cannot VACUUM from within a transaction self._autocommit = False # Execute statement @@ -389,7 +390,7 @@ def teardown_appcontext(exception): connection.execute(sqlalchemy.text("COMMIT")) # Check for end of transaction - if command in ["COMMIT", "ROLLBACK"]: + if command in ["COMMIT", "ROLLBACK", "VACUUM"]: # cannot VACUUM from within a transaction self._autocommit = True # Return value From f81a0a3920d90296537843d45683ec0b5d70171b Mon Sep 17 00:00:00 2001 From: "David J. Malan" <malan@harvard.edu> Date: Mon, 14 Oct 2024 20:40:31 -0400 Subject: [PATCH 44/44] fixes raw strings --- src/cs50/sql.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cs50/sql.py b/src/cs50/sql.py index a133293..81905bc 100644 --- a/src/cs50/sql.py +++ b/src/cs50/sql.py @@ -329,12 +329,12 @@ def execute(self, sql, *args, **kwargs): sqlparse.tokens.Literal.String, sqlparse.tokens.Literal.String.Single, ]: - token.value = re.sub("(^'|\s+):", r"\1\:", token.value) + token.value = re.sub(r"(^'|\s+):", r"\1\:", token.value) # In identifier # https://www.sqlite.org/lang_keywords.html elif token.ttype == sqlparse.tokens.Literal.String.Symbol: - token.value = re.sub('(^"|\s+):', r"\1\:", token.value) + token.value = re.sub(r'(^"|\s+):', r"\1\:", token.value) # Join tokens into statement statement = "".join([str(token) for token in tokens])