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 01/13] 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 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 02/13] 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 03/13] 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 04/13] 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 05/13] 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 06/13] 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 07/13] 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 08/13] 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 09/13] 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 10/13] 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 11/13] 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]) From b629b6dcdf3041982117801a35c01be2b72cb3d3 Mon Sep 17 00:00:00 2001 From: "David J. Malan" <malan@harvard.edu> Date: Sat, 5 Apr 2025 11:36:15 -0400 Subject: [PATCH 12/13] Update README.md --- README.md | 47 ----------------------------------------------- 1 file changed, 47 deletions(-) diff --git a/README.md b/README.md index c94bd1f..a9033c6 100644 --- a/README.md +++ b/README.md @@ -39,50 +39,3 @@ s = cs50.get_string(); ``` python tests/sql.py ``` - -### Sample Tests - -``` -import cs50 -db = cs50.SQL("sqlite:///foo.db") -db.execute("CREATE TABLE IF NOT EXISTS cs50 (id INTEGER PRIMARY KEY, val TEXT, bin BLOB)") -db.execute("INSERT INTO cs50 (val) VALUES('a')") -db.execute("INSERT INTO cs50 (val) VALUES('b')") -db.execute("BEGIN") -db.execute("INSERT INTO cs50 (val) VALUES('c')") -db.execute("INSERT INTO cs50 (val) VALUES('x')") -db.execute("INSERT INTO cs50 (val) VALUES('y')") -db.execute("ROLLBACK") -db.execute("INSERT INTO cs50 (val) VALUES('z')") -db.execute("COMMIT") - ---- - -import cs50 -db = cs50.SQL("mysql://root@localhost/test") -db.execute("CREATE TABLE IF NOT EXISTS cs50 (id INTEGER PRIMARY KEY, val TEXT, bin BLOB)") -db.execute("INSERT INTO cs50 (val) VALUES('a')") -db.execute("INSERT INTO cs50 (val) VALUES('b')") -db.execute("BEGIN") -db.execute("INSERT INTO cs50 (val) VALUES('c')") -db.execute("INSERT INTO cs50 (val) VALUES('x')") -db.execute("INSERT INTO cs50 (val) VALUES('y')") -db.execute("ROLLBACK") -db.execute("INSERT INTO cs50 (val) VALUES('z')") -db.execute("COMMIT") - ---- - -import cs50 -db = cs50.SQL("postgresql://postgres@localhost/test") -db.execute("CREATE TABLE IF NOT EXISTS cs50 (id SERIAL PRIMARY KEY, val VARCHAR(16), bin BYTEA)") -db.execute("INSERT INTO cs50 (val) VALUES('a')") -db.execute("INSERT INTO cs50 (val) VALUES('b')") -db.execute("BEGIN") -db.execute("INSERT INTO cs50 (val) VALUES('c')") -db.execute("INSERT INTO cs50 (val) VALUES('x')") -db.execute("INSERT INTO cs50 (val) VALUES('y')") -db.execute("ROLLBACK") -db.execute("INSERT INTO cs50 (val) VALUES('z')") -db.execute("COMMIT") -``` From 68f4380757d8adb92d027c42c2099e798bff9488 Mon Sep 17 00:00:00 2001 From: "David J. Malan" <malan@harvard.edu> Date: Sat, 5 Apr 2025 11:38:49 -0400 Subject: [PATCH 13/13] updated Python for 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 0b0ee1a..7fcb507 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -23,7 +23,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: '3.7' + python-version: '3.12' check-latest: true - name: Setup databases run: |