From 78ad44bbb6703dedee2f31c34eb863b9fe870d53 Mon Sep 17 00:00:00 2001 From: "David J. Malan" <malan@harvard.edu> Date: Sun, 9 Apr 2017 17:42:00 -0400 Subject: [PATCH 01/35] added support for optional args for parity with newest C library --- cs50/cs50.py | 40 +++++++++++++++++++++++++++------------- setup.py | 2 +- test/python2.py | 2 -- test/python3.py | 2 -- 4 files changed, 28 insertions(+), 18 deletions(-) diff --git a/cs50/cs50.py b/cs50/cs50.py index 657f405..d9c2923 100644 --- a/cs50/cs50.py +++ b/cs50/cs50.py @@ -20,20 +20,23 @@ def write(self, x): sys.stderr = flushfile(sys.stderr) sys.stdout = flushfile(sys.stdout) -def get_char(): +def get_char(prompt=None): """Read a line of text from standard input and return the equivalent char.""" while True: - s = get_string() + s = get_string(prompt) if s is None: return None if len(s) == 1: return s[0] - print("Retry: ", end="") -def get_float(): + # temporarily here for backwards compatibility + if prompt is None: + print("Retry: ", end="") + +def get_float(prompt=None): """Read a line of text from standard input and return the equivalent float.""" while True: - s = get_string() + s = get_string(prompt) if s is None: return None if len(s) > 0 and re.search(r"^[+-]?\d*(?:\.\d*)?$", s): @@ -41,12 +44,15 @@ def get_float(): return float(s) except ValueError: pass - print("Retry: ", end="") -def get_int(): + # temporarily here for backwards compatibility + if prompt is None: + print("Retry: ", end="") + +def get_int(prompt=None): """Read a line of text from standard input and return the equivalent int.""" while True: - s = get_string(); + s = get_string(prompt); if s is None: return None if re.search(r"^[+-]?\d+$", s): @@ -56,13 +62,16 @@ def get_int(): return i except ValueError: pass - print("Retry: ", end="") + + # temporarily here for backwards compatibility + if prompt is None: + print("Retry: ", end="") if sys.version_info.major != 3: - def get_long(): + def get_long(prompt=None): """Read a line of text from standard input and return the equivalent long.""" while True: - s = get_string(); + s = get_string(prompt) if s is None: return None if re.search(r"^[+-]?\d+$", s): @@ -70,11 +79,16 @@ def get_long(): return long(s, 10) except ValueError: pass - print("Retry: ", end="") -def get_string(): + # temporarily here for backwards compatibility + if prompt is None: + print("Retry: ", end="") + +def get_string(prompt=None): """Read a line of text from standard input and return it as a string.""" try: + if prompt is not None: + print(prompt, end="") s = sys.stdin.readline() return re.sub(r"(?:\r|\r\n|\n)$", "", s) except ValueError: diff --git a/setup.py b/setup.py index cee2266..09c4c5e 100644 --- a/setup.py +++ b/setup.py @@ -15,5 +15,5 @@ name="cs50", packages=["cs50"], url="https://github.com/cs50/python-cs50", - version="1.3.0" + version="2.0.0" ) diff --git a/test/python2.py b/test/python2.py index 10f19e4..69de822 100644 --- a/test/python2.py +++ b/test/python2.py @@ -1,6 +1,4 @@ import cs50 -from cs50 import SQL - l = cs50.get_long() print(l) diff --git a/test/python3.py b/test/python3.py index efef779..b545b98 100644 --- a/test/python3.py +++ b/test/python3.py @@ -1,6 +1,4 @@ import cs50 -from cs50 import SQL i = cs50.get_int() print(i) - From ea74fd7a49c517a04ddae0366fa626c79f0eb058 Mon Sep 17 00:00:00 2001 From: Kareem Zidane <kzidane@cs50.harvard.edu> Date: Fri, 14 Apr 2017 13:02:41 +0200 Subject: [PATCH 02/35] deploying on master and simplified makefile --- .travis.yml | 24 +++++------------------- Makefile | 11 ----------- 2 files changed, 5 insertions(+), 30 deletions(-) diff --git a/.travis.yml b/.travis.yml index b766e01..4e8c279 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,24 +11,10 @@ before_deploy: pip install twine deploy: # create github release - - provider: releases - - # GitHub access token - api_key: - secure: "pwBn1lky58GHp1qR4i0oSZyOJkGMTvRzt0EfESVtl2ZbRTVUE7UFQbk/cL1002zMOUkJ4c5IjRCg95NJZWUvOSHwt4cOyqDVzi/S6AqACOlOhfnkw3S6oLGgRT3IlXK2ng0Viiqc1+BlVdwwXURKyobirqFgr4MAlb5kh75WmV9Xs4GwIS+qPq9luv38Bls2US/mNt5KRV1DiePr2ZSCqFESFfoIz+QKhZtVdynEF4jJwevEwP4HrCoT3guIJlXcWhOG37n+e8S4YLwg+k3yYeQTmR/QMgjmQLwEBZ6v9bNjqXM3CMtn3KUryDzcp5Z5+Vv1p1uoDbmuK+Ll5nQttAp/gARk+IWZ/xWc8MuQpFvjRzafbtPiF7ZlqaYh1wCREuZTWDAk/UJgQxb81v0jo0iAPyk9HMfgK2CJuU8wDwraKZ5dKk4y45Zww1gSSzpJJ8xSrylKPn7Wnft617Lnc+O0X6DnIAFtHDAPu/lPFaaokn1TN9AOPXoxb2cEeh+oDcUQD4zZG6Ukvh9+Hw8XiFBG+jEm6ekCvawTjnlZmBIw8YPJKEjrZv8LWfKhnVebRbmehawmnrZxUALCp39EjrcsIltYw4gefbd/Z9kIr8r3yVZfuq7U6vd8PBuCiDZHlKM1Lz4Ns24WK96nYe6V9Lt3WUERh6xt8JtuFrNHBiQ=" - - # enable wildcards in filenames - file_glob: true - - # upload sdist - file: dist/* - - # avoid stashing sdist - skip_cleanup: true - - # create releases on tags only + - provider: script + script: 'curl --fail --data "{ \"tag_name\": \"v$(python setup.py --version)\", \"target_commitish\": \"$TRAVIS_COMMIT\", \"name\": \"v$(python setup.py --version)\" }" --user bot50:$GITHUB_TOKEN https://api.github.com/repos/$TRAVIS_REPO_SLUG/releases' on: - tags: true + branch: master # deploy to PyPI - provider: script @@ -39,6 +25,6 @@ deploy: # avoid stashing sdist skip_cleanup: true - # deploy on tags only + # deploy on commits to master on: - tags: true + branch: master diff --git a/Makefile b/Makefile index 415b084..063b9de 100644 --- a/Makefile +++ b/Makefile @@ -9,14 +9,3 @@ clean: .PHONY: install install: build pip install dist/*.tar.gz - -.PHONY: push -push: - git push origin "v$$(python setup.py --version)" - -.PHONY: release -release: tag push - -.PHONY: tag -tag: - git tag "v$$(python setup.py --version)" From a6a43ed3547b1f0a6842d90efc092934a650461c Mon Sep 17 00:00:00 2001 From: Kareem Zidane <kzidane@cs50.harvard.edu> Date: Tue, 18 Apr 2017 04:25:55 +0200 Subject: [PATCH 03/35] disabled tag builds --- .travis.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index b766e01..003bda6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,10 @@ language: python python: "3.4" +# don't build tags +branches: + except: /^v\d/ + # build sdist script: make build From d8a74a34fd5ce16c23b8a13b0dbbbc96d530bb19 Mon Sep 17 00:00:00 2001 From: "David J. Malan" <malan@harvard.edu> Date: Sat, 29 Apr 2017 11:47:52 -0400 Subject: [PATCH 04/35] returning None for EOF, updated comments --- cs50/cs50.py | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/cs50/cs50.py b/cs50/cs50.py index d9c2923..edcb52d 100644 --- a/cs50/cs50.py +++ b/cs50/cs50.py @@ -21,7 +21,11 @@ def write(self, x): sys.stdout = flushfile(sys.stdout) def get_char(prompt=None): - """Read a line of text from standard input and return the equivalent char.""" + """ + Read a line of text from standard input and return the equivalent char; + if text is not a single char, user is prompted to retry. If line can't + be read, return None. + """ while True: s = get_string(prompt) if s is None: @@ -34,7 +38,11 @@ def get_char(prompt=None): print("Retry: ", end="") def get_float(prompt=None): - """Read a line of text from standard input and return the equivalent float.""" + """ + Read a line of text from standard input and return the equivalent float + as precisely as possible; if text does not represent a double, user is + prompted to retry. If line can't be read, return None. + """ while True: s = get_string(prompt) if s is None: @@ -50,7 +58,11 @@ def get_float(prompt=None): print("Retry: ", end="") def get_int(prompt=None): - """Read a line of text from standard input and return the equivalent int.""" + """ + Read a line of text from standard input and return the equivalent int; + if text does not represent an int, user is prompted to retry. If line + can't be read, return None. + """ while True: s = get_string(prompt); if s is None: @@ -69,7 +81,11 @@ def get_int(prompt=None): if sys.version_info.major != 3: def get_long(prompt=None): - """Read a line of text from standard input and return the equivalent long.""" + """ + Read a line of text from standard input and return the equivalent long; + if text does not represent a long, user is prompted to retry. If line + can't be read, return None. + """ while True: s = get_string(prompt) if s is None: @@ -85,11 +101,18 @@ def get_long(prompt=None): print("Retry: ", end="") def get_string(prompt=None): - """Read a line of text from standard input and return it as a string.""" + """ + Read a line of text from standard input and return it as a string, + sans trailing line ending. Supports CR (\r), LF (\n), and CRLF (\r\n) + 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). + """ try: if prompt is not None: print(prompt, end="") s = sys.stdin.readline() + if not s: + return None return re.sub(r"(?:\r|\r\n|\n)$", "", s) except ValueError: return None From 9f7dca1715bc51416fa55b677c352675817a5d36 Mon Sep 17 00:00:00 2001 From: "David J. Malan" <malan@harvard.edu> Date: Sat, 29 Apr 2017 12:11:32 -0400 Subject: [PATCH 05/35] added eprint --- cs50/cs50.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/cs50/cs50.py b/cs50/cs50.py index 657f405..5f670ae 100644 --- a/cs50/cs50.py +++ b/cs50/cs50.py @@ -1,4 +1,5 @@ from __future__ import print_function +import inspect import re import sys @@ -20,6 +21,15 @@ def write(self, x): sys.stderr = flushfile(sys.stderr) sys.stdout = flushfile(sys.stdout) +def eprint(*objects, end="\n", sep=" "): + """ + Print an error message to standard error, prefixing it with caller's + file name and line number. + """ + (frame, filename, lineno, function, code_context, index) = inspect.stack()[1] + print("{}:{}: ".format(filename, lineno), end="") + print(*objects, end=end, file=sys.stderr, sep=sep) + def get_char(): """Read a line of text from standard input and return the equivalent char.""" while True: From bc1483abc61bc3aca029998583afbf2519f276a8 Mon Sep 17 00:00:00 2001 From: "David J. Malan" <malan@harvard.edu> Date: Sat, 29 Apr 2017 12:14:46 -0400 Subject: [PATCH 06/35] clarified eprint's comments --- cs50/cs50.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cs50/cs50.py b/cs50/cs50.py index 5f670ae..bae76a7 100644 --- a/cs50/cs50.py +++ b/cs50/cs50.py @@ -23,8 +23,8 @@ def write(self, x): def eprint(*objects, end="\n", sep=" "): """ - Print an error message to standard error, prefixing it with caller's - file name and line number. + Print an error message to standard error, prefixing it with + file name and line number from which method was called. """ (frame, filename, lineno, function, code_context, index) = inspect.stack()[1] print("{}:{}: ".format(filename, lineno), end="") From bfbe74d410e4dabb3ef200df914fc1de796c7f82 Mon Sep 17 00:00:00 2001 From: Kareem Zidane <kzidane@cs50.harvard.edu> Date: Fri, 12 May 2017 22:34:29 +0200 Subject: [PATCH 07/35] configured slack notifications [skip ci] --- .travis.yml | 45 ++++++++++++++++----------------------------- 1 file changed, 16 insertions(+), 29 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6e383b5..b37c08e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,34 +1,21 @@ language: python - -python: "3.4" - -# don't build tags +python: '3.4' branches: - except: /^v\d/ - -# build sdist + except: "/^v\\d/" script: make build - -# install twine for uploading to PyPI before_deploy: pip install twine - deploy: - - # create github release - - provider: script - script: 'curl --fail --data "{ \"tag_name\": \"v$(python setup.py --version)\", \"target_commitish\": \"$TRAVIS_COMMIT\", \"name\": \"v$(python setup.py --version)\" }" --user bot50:$GITHUB_TOKEN https://api.github.com/repos/$TRAVIS_REPO_SLUG/releases' - on: - branch: master - - # deploy to PyPI - - provider: script - - # upload sdist to PyPI - script: twine upload -u $PYPI_USERNAME -p $PYPI_PASSWORD dist/* - - # avoid stashing sdist - skip_cleanup: true - - # deploy on commits to master - on: - branch: master +- provider: script + script: 'curl --fail --data "{ \"tag_name\": \"v$(python setup.py --version)\", + \"target_commitish\": \"$TRAVIS_COMMIT\", \"name\": \"v$(python setup.py --version)\" + }" --user bot50:$GITHUB_TOKEN https://api.github.com/repos/$TRAVIS_REPO_SLUG/releases' + on: + branch: master +- provider: script + script: twine upload -u $PYPI_USERNAME -p $PYPI_PASSWORD dist/* + skip_cleanup: true + on: + branch: master +notifications: + slack: + secure: lJklhcBVjDT6KzUNa3RFHXdXSeH7ytuuGrkZ5ZcR72CXMoTf2pMJTzPwRLWOp6lCSdDC9Y8MWLrcg/e33dJga4Jlp9alOmWqeqesaFjfee4st8vAsgNbv8/RajPH1gD2bnkt8oIwUzdHItdb5AucKFYjbH2g0d8ndoqYqUeBLrnsT1AP5G/Vi9OHC9OWNpR0FKaZIJE0Wt52vkPMH3sV2mFeIskByPB+56U5y547mualKxn61IVR/dhYBEtZQJuSvnwKHPOn9Pkk7cCa+SSSeTJ4w5LboY8T17otaYNauXo46i1bKIoGiBcCcrJyQHHiPQmcq/YU540MC5Wzt9YXUycmJzRi347oyQeDee27wV3XJlWMXuuhbtJiKCFny7BTQ160VATlj/dbwIzN99Ra6/BtTumv/6LyTdKIuVjdAkcN8dtdDW1nlrQ29zuPNCcXXzJ7zX7kQaOCUV1c2OrsbiH/0fE9nknUORn97txqhlYVi0QMS7764wFo6kg0vpmFQRkkQySsJl+TmgcZ01AlsJc2EMMWVuaj9Af9JU4/4yalqDiXIh1fOYYUZnLfOfWS+MsnI+/oLfqJFyMbrsQQTIjs+kTzbiEdhd2R4EZgusU/xRFWokS2NAvahexrRhRQ6tpAI+LezPrkNOR3aHiykBf+P9BkUa0wPp6V2Ayc6q0= From a94b031774886395d0a5e1afbd47fefdd4a947ab Mon Sep 17 00:00:00 2001 From: Kareem Zidane <kzidane@cs50.harvard.edu> Date: Fri, 12 May 2017 22:40:03 +0200 Subject: [PATCH 08/35] deploying using pypi integration [skip ci] --- .travis.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index b37c08e..f097728 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,8 +2,8 @@ language: python python: '3.4' branches: except: "/^v\\d/" -script: make build -before_deploy: pip install twine +install: true +script: true deploy: - provider: script script: 'curl --fail --data "{ \"tag_name\": \"v$(python setup.py --version)\", @@ -11,9 +11,9 @@ deploy: }" --user bot50:$GITHUB_TOKEN https://api.github.com/repos/$TRAVIS_REPO_SLUG/releases' on: branch: master -- provider: script - script: twine upload -u $PYPI_USERNAME -p $PYPI_PASSWORD dist/* - skip_cleanup: true +- provider: pypi + user: "$PYPI_USERNAME" + password: "$PYPI_PASSWORD" on: branch: master notifications: From 93fa5999eda49f8f4ff6d751c2a1732b2f8f086d Mon Sep 17 00:00:00 2001 From: Kareem Zidane <kzidane@cs50.harvard.edu> Date: Fri, 12 May 2017 23:42:21 +0200 Subject: [PATCH 09/35] removed makefile and updated readme --- Makefile | 11 ----------- README.md | 16 +++------------- 2 files changed, 3 insertions(+), 24 deletions(-) delete mode 100644 Makefile diff --git a/Makefile b/Makefile deleted file mode 100644 index 063b9de..0000000 --- a/Makefile +++ /dev/null @@ -1,11 +0,0 @@ -.PHONY: build -build: clean - python setup.py sdist - -.PHONY: clean -clean: - rm -rf *.egg-info dist - -.PHONY: install -install: build - pip install dist/*.tar.gz diff --git a/README.md b/README.md index 3f51734..fa498f1 100644 --- a/README.md +++ b/README.md @@ -4,19 +4,11 @@ Supports Python 2 and Python 3. -## Development - -Requires [Docker Engine](https://docs.docker.com/engine/installation/). - - make bash - make deb # builds .deb - ## Installation -1. Download the latest release per https://github.com/cs50/python-cs50/releases -1. Extract `python-cs50-*` -1. `cd python-cs50-*` -1. `make install` +``` +pip install cs50 +``` ## Usage @@ -31,8 +23,6 @@ Requires [Docker Engine](https://docs.docker.com/engine/installation/). s = cs50.get_string(); ## TODO - -* Add install target to Makefile. * Conditionally install for Python 2 and/or Python 3. * Add targets for `pacman`, `rpm`. * Add tests. From 19b33c09cf1e9c0cdf1bfdb303a0495e18539b80 Mon Sep 17 00:00:00 2001 From: "David J. Malan" <malan@harvard.edu> Date: Sat, 20 May 2017 11:53:24 -0400 Subject: [PATCH 10/35] added dependency --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index cee2266..28ed25a 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ "Topic :: Software Development :: Libraries :: Python Modules" ], description="CS50 library for Python", - install_requires=["SQLAlchemy"], + install_requires=["SQLAlchemy", "sqlparse"], keywords="cs50", name="cs50", packages=["cs50"], From 508937a54c09056b8f3d7c9c8306f213d2f56875 Mon Sep 17 00:00:00 2001 From: "David J. Malan" <malan@harvard.edu> Date: Sat, 20 May 2017 19:51:12 -0400 Subject: [PATCH 11/35] trying support for all paramstyles --- cs50/sql.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/cs50/sql.py b/cs50/sql.py index 2064133..1ad0664 100644 --- a/cs50/sql.py +++ b/cs50/sql.py @@ -1,4 +1,6 @@ +import re import sqlalchemy +import sqlparse class SQL(object): """Wrap SQLAlchemy to provide a simple SQL API.""" @@ -20,8 +22,45 @@ def execute(self, text, *multiparams, **params): """ Execute a SQL statement. """ + + # parse text + parsed = sqlparse.parse(text) + if len(parsed) == 0: + raise RuntimeError("missing statement") + elif len(parsed) > 1: + raise RuntimeError("too many statements") + statement = parsed[0] + if statement.get_type() == "UNKNOWN": + raise RuntimeError("unknown type of statement") + + # infer paramstyle + # https://www.python.org/dev/peps/pep-0249/#paramstyle + paramstyle = None + for token in statement.flatten(): + if sqlparse.utils.imt(token.ttype, t=sqlparse.tokens.Token.Name.Placeholder): + _paramstyle = None + if re.search(r"^\?$", token.value): + _paramstyle = "qmark" + elif re.search(r"^:\d+$", token.value): + _paramstyle = "numeric" + elif re.search(r"^:\w+$", token.value): + _paramstyle = "named" + elif re.search(r"^%s$", token.value): + _paramstyle = "format" + elif re.search(r"^%\(\w+\)s$", token.value): + _paramstyle = "pyformat" + else: + raise RuntimeError("unknown paramstyle") + if paramstyle and paramstyle != _paramstyle: + raise RuntimeError("inconsistent paramstyle") + paramstyle = _paramstyle + try: + parsed = sqlparse.split("SELECT * FROM cs50 WHERE id IN (SELECT id FROM cs50); SELECT 1; CREATE TABLE foo") + print(parsed) + return 0 + # bind parameters before statement reaches database, so that bound parameters appear in exceptions # http://docs.sqlalchemy.org/en/latest/core/sqlelement.html#sqlalchemy.sql.expression.text # https://groups.google.com/forum/#!topic/sqlalchemy/FfLwKT1yQlg From ab5d8366c6af32eaf76b3e54b225b7f9f33f79e0 Mon Sep 17 00:00:00 2001 From: "David J. Malan" <malan@harvard.edu> Date: Sat, 20 May 2017 21:51:39 -0400 Subject: [PATCH 12/35] added support for expandable parameters --- cs50/sql.py | 120 +++++++++++++++++++++++++++++++++------------------- 1 file changed, 77 insertions(+), 43 deletions(-) diff --git a/cs50/sql.py b/cs50/sql.py index 1ad0664..fa9a864 100644 --- a/cs50/sql.py +++ b/cs50/sql.py @@ -1,6 +1,6 @@ -import re +import datetime import sqlalchemy -import sqlparse +import sys class SQL(object): """Wrap SQLAlchemy to provide a simple SQL API.""" @@ -16,58 +16,91 @@ def __init__(self, url): try: self.engine = sqlalchemy.create_engine(url) except Exception as e: + e.__context__ = None raise RuntimeError(e) - def execute(self, text, *multiparams, **params): + def execute(self, text, **params): """ Execute a SQL statement. """ - # parse text - parsed = sqlparse.parse(text) - if len(parsed) == 0: - raise RuntimeError("missing statement") - elif len(parsed) > 1: - raise RuntimeError("too many statements") - statement = parsed[0] - if statement.get_type() == "UNKNOWN": - raise RuntimeError("unknown type of statement") - - # infer paramstyle - # https://www.python.org/dev/peps/pep-0249/#paramstyle - paramstyle = None - for token in statement.flatten(): - if sqlparse.utils.imt(token.ttype, t=sqlparse.tokens.Token.Name.Placeholder): - _paramstyle = None - if re.search(r"^\?$", token.value): - _paramstyle = "qmark" - elif re.search(r"^:\d+$", token.value): - _paramstyle = "numeric" - elif re.search(r"^:\w+$", token.value): - _paramstyle = "named" - elif re.search(r"^%s$", token.value): - _paramstyle = "format" - elif re.search(r"^%\(\w+\)s$", token.value): - _paramstyle = "pyformat" - else: - raise RuntimeError("unknown paramstyle") - if paramstyle and paramstyle != _paramstyle: - raise RuntimeError("inconsistent paramstyle") - paramstyle = _paramstyle + class UserDefinedType(sqlalchemy.TypeDecorator): + """ + Add support for expandable values, a la https://bitbucket.org/zzzeek/sqlalchemy/issues/3953/expanding-parameter. + """ + impl = sqlalchemy.types.UserDefinedType + def process_literal_param(self, value, dialect): + """Receive a literal parameter value to be rendered inline within a statement.""" + def process(value): + """Render a literal value, escaping as needed.""" + + # bool + if isinstance(value, bool): + return sqlalchemy.types.Boolean().literal_processor(dialect)(value) + + # datetime.date + elif isinstance(value, datetime.date): + return sqlalchemy.types.String().literal_processor(dialect)(value.strftime("%Y-%m-%d")) + + # datetime.datetime + elif isinstance(value, datetime.datetime): + return sqlalchemy.types.String().literal_processor(dialect)(value.strftime("%Y-%m-%d %H:%M:%S")) + + # datetime.time + elif isinstance(value, datetime.time): + return sqlalchemy.types.String().literal_processor(dialect)(value.strftime("%H:%M:%S")) + + # float + elif isinstance(value, float): + return sqlalchemy.types.Float().literal_processor(dialect)(value) + + # int + elif isinstance(value, int): + return sqlalchemy.types.Integer().literal_processor(dialect)(value) + + # long + elif sys.version_info.major != 3 and isinstance(value, long): + return sqlalchemy.types.Integer().literal_processor(dialect)(value) + + # str + elif isinstance(value, str): + return sqlalchemy.types.String().literal_processor(dialect)(value) + + # None + elif isinstance(value, sqlalchemy.sql.elements.Null): + return sqlalchemy.types.NullType().literal_processor(dialect)(value) + + # unsupported value + raise RuntimeError("unsupported value") + + # process value(s), separating with commas as needed + if type(value) is list: + return ", ".join([process(v) for v in value]) + else: + return process(value) try: - parsed = sqlparse.split("SELECT * FROM cs50 WHERE id IN (SELECT id FROM cs50); SELECT 1; CREATE TABLE foo") - print(parsed) - return 0 + # construct a new TextClause clause + statement = sqlalchemy.text(text) + + # iterate over parameters + for key, value in params.items(): - # bind parameters before statement reaches database, so that bound parameters appear in exceptions - # http://docs.sqlalchemy.org/en/latest/core/sqlelement.html#sqlalchemy.sql.expression.text - # https://groups.google.com/forum/#!topic/sqlalchemy/FfLwKT1yQlg - # http://docs.sqlalchemy.org/en/latest/core/connections.html#sqlalchemy.engine.Engine.execute + # translate None to NULL + if value is None: + value = sqlalchemy.sql.null() + + # bind parameters before statement reaches database, so that bound parameters appear in exceptions + # http://docs.sqlalchemy.org/en/latest/core/sqlelement.html#sqlalchemy.sql.expression.text + statement = statement.bindparams(sqlalchemy.bindparam(key, value=value, type_=UserDefinedType())) + + # stringify bound parameters # http://docs.sqlalchemy.org/en/latest/faq/sqlexpressions.html#how-do-i-render-sql-expressions-as-strings-possibly-with-bound-parameters-inlined - statement = sqlalchemy.text(text).bindparams(*multiparams, **params) - result = self.engine.execute(str(statement.compile(compile_kwargs={"literal_binds": True}))) + self.statement = str(statement.compile(compile_kwargs={"literal_binds": True})) + + # execute statement + result = self.engine.execute(self.statement) # if SELECT (or INSERT with RETURNING), return result set as list of dict objects if result.returns_rows: @@ -88,4 +121,5 @@ def execute(self, text, *multiparams, **params): # else raise error except Exception as e: + e.__context__ = None raise RuntimeError(e) From 64c1a04e2700378e37fb7853dc1a8fe89a4934e6 Mon Sep 17 00:00:00 2001 From: "David J. Malan" <malan@harvard.edu> Date: Sat, 20 May 2017 21:51:45 -0400 Subject: [PATCH 13/35] removed sqlparse --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 28ed25a..cee2266 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ "Topic :: Software Development :: Libraries :: Python Modules" ], description="CS50 library for Python", - install_requires=["SQLAlchemy", "sqlparse"], + install_requires=["SQLAlchemy"], keywords="cs50", name="cs50", packages=["cs50"], From 71dea16d78c8fa117162b937516079b4d44fe951 Mon Sep 17 00:00:00 2001 From: "David J. Malan" <malan@harvard.edu> Date: Sun, 21 May 2017 00:19:17 -0400 Subject: [PATCH 14/35] fixed support for PostgreSQL --- cs50/sql.py | 41 ++++++++++----- test/sqltests.py | 129 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 157 insertions(+), 13 deletions(-) create mode 100644 test/sqltests.py diff --git a/cs50/sql.py b/cs50/sql.py index fa9a864..036e17c 100644 --- a/cs50/sql.py +++ b/cs50/sql.py @@ -1,22 +1,25 @@ import datetime +import re import sqlalchemy import sys +import warnings class SQL(object): """Wrap SQLAlchemy to provide a simple SQL API.""" - def __init__(self, url): + def __init__(self, url, **kwargs): """ Create instance of sqlalchemy.engine.Engine. URL should be a string that indicates database dialect and connection arguments. http://docs.sqlalchemy.org/en/latest/core/engines.html#sqlalchemy.create_engine + http://docs.sqlalchemy.org/en/latest/dialects/index.html """ try: - self.engine = sqlalchemy.create_engine(url) + self.engine = sqlalchemy.create_engine(url, **kwargs) except Exception as e: - e.__context__ = None + e.__cause__ = None raise RuntimeError(e) def execute(self, text, **params): @@ -79,6 +82,10 @@ def process(value): else: return process(value) + # raise exceptions for warnings + warnings.filterwarnings("error") + + # prepare, execute statement try: # construct a new TextClause clause @@ -97,29 +104,37 @@ def process(value): # stringify bound parameters # http://docs.sqlalchemy.org/en/latest/faq/sqlexpressions.html#how-do-i-render-sql-expressions-as-strings-possibly-with-bound-parameters-inlined - self.statement = str(statement.compile(compile_kwargs={"literal_binds": True})) + statement = str(statement.compile(compile_kwargs={"literal_binds": True})) # execute statement - result = self.engine.execute(self.statement) + result = self.engine.execute(statement) # if SELECT (or INSERT with RETURNING), return result set as list of dict objects - if result.returns_rows: + if re.search(r"^\s*SELECT\s+", statement, re.I): rows = result.fetchall() return [dict(row) for row in rows] # if INSERT, return primary key value for a newly inserted row - elif result.lastrowid is not None: - return result.lastrowid + elif re.search(r"^\s*INSERT\s+", statement, re.I): + if self.engine.url.get_backend_name() == "postgresql": + result = self.engine.execute(sqlalchemy.text("SELECT LASTVAL()")) + return result.first()[0] + else: + return result.lastrowid - # if DELETE or UPDATE (or INSERT without RETURNING), return number of rows matched - else: + # if DELETE or UPDATE, return number of rows matched + elif re.search(r"^\s*(?:DELETE|UPDATE)\s+", statement, re.I): return result.rowcount + # if some other statement, return True unless exception + return True + # if constraint violated, return None except sqlalchemy.exc.IntegrityError: return None - # else raise error + # else raise exception except Exception as e: - e.__context__ = None - raise RuntimeError(e) + _e = RuntimeError(e) # else Python 3 prints warnings' tracebacks + _e.__cause__ = None + raise _e diff --git a/test/sqltests.py b/test/sqltests.py new file mode 100644 index 0000000..d2204a1 --- /dev/null +++ b/test/sqltests.py @@ -0,0 +1,129 @@ +import unittest +from cs50.sql import SQL + +class SQLTests(unittest.TestCase): + def test_delete_returns_affected_rows(self): + rows = [ + {"id": 1, "val": "foo"}, + {"id": 2, "val": "bar"}, + {"id": 3, "val": "baz"} + ] + for row in rows: + self.db.execute("INSERT INTO cs50(val) VALUES(:val);", val=row["val"]) + + print(self.db.execute("DELETE FROM cs50 WHERE id = :id", id=rows[0]["id"])) + print(self.db.execute("SELECT * FROM cs50")) + return + + self.assertEqual(self.db.execute("DELETE FROM cs50 WHERE id = :id", id=rows[0]["id"]), 1) + self.assertEqual(self.db.execute("DELETE FROM cs50 WHERE id = :a or id = :b", a=rows[1]["id"], b=rows[2]["id"]), 2) + self.assertEqual(self.db.execute("DELETE FROM cs50 WHERE id = -50"), 0) + + def test_insert_returns_last_row_id(self): + self.assertEqual(self.db.execute("INSERT INTO cs50(val) VALUES('foo')"), 1) + self.assertEqual(self.db.execute("INSERT INTO cs50(val) VALUES('bar')"), 2) + + def test_select_all(self): + self.assertEqual(self.db.execute("SELECT * FROM cs50"), []) + + rows = [ + {"id": 1, "val": "foo"}, + {"id": 2, "val": "bar"}, + {"id": 3, "val": "baz"} + ] + for row in rows: + self.db.execute("INSERT INTO cs50(val) VALUES(:val)", val=row["val"]) + + self.assertEqual(self.db.execute("SELECT * FROM cs50"), rows) + + def test_select_cols(self): + rows = [ + {"val": "foo"}, + {"val": "bar"}, + {"val": "baz"} + ] + for row in rows: + self.db.execute("INSERT INTO cs50(val) VALUES(:val)", val=row["val"]) + + self.assertEqual(self.db.execute("SELECT val FROM cs50"), rows) + + def test_select_where(self): + rows = [ + {"id": 1, "val": "foo"}, + {"id": 2, "val": "bar"}, + {"id": 3, "val": "baz"} + ] + for row in rows: + self.db.execute("INSERT INTO cs50(val) VALUES(:val)", val=row["val"]) + + self.assertEqual(self.db.execute("SELECT * FROM cs50 WHERE id = :id OR val = :val", id=rows[1]["id"], val=rows[2]["val"]), rows[1:3]) + + def test_update_returns_affected_rows(self): + rows = [ + {"id": 1, "val": "foo"}, + {"id": 2, "val": "bar"}, + {"id": 3, "val": "baz"} + ] + for row in rows: + self.db.execute("INSERT INTO cs50(val) VALUES(:val)", val=row["val"]) + + self.assertEqual(self.db.execute("UPDATE cs50 SET val = 'foo' WHERE id > 1"), 2) + self.assertEqual(self.db.execute("UPDATE cs50 SET val = 'foo' WHERE id = -50"), 0) + +class MySQLTests(SQLTests): + @classmethod + def setUpClass(self): + self.db = SQL("mysql://root@localhost/cs50_sql_tests") + + def setUp(self): + self.db.execute("CREATE TABLE cs50 (id INTEGER NOT NULL AUTO_INCREMENT, val VARCHAR(16), PRIMARY KEY (id))") + + def tearDown(self): + self.db.execute("DROP TABLE cs50") + + @classmethod + def tearDownClass(self): + self.db.execute("DROP TABLE IF EXISTS cs50") + +class PostgresTests(SQLTests): + @classmethod + def setUpClass(self): + self.db = SQL("postgresql://postgres:postgres@localhost/cs50_sql_tests") + + def setUp(self): + self.db.execute("CREATE TABLE cs50 (id SERIAL PRIMARY KEY, val VARCHAR(16))") + + def tearDown(self): + self.db.execute("DROP TABLE cs50") + + @classmethod + def tearDownClass(self): + self.db.execute("DROP TABLE IF EXISTS cs50") + + def test_insert_returns_last_row_id(self): + self.assertEqual(self.db.execute("INSERT INTO cs50(val) VALUES('foo')"), 1) + self.assertEqual(self.db.execute("INSERT INTO cs50(val) VALUES('bar')"), 2) + +class SQLiteTests(SQLTests): + @classmethod + def setUpClass(self): + self.db = SQL("sqlite:///cs50_sql_tests.db") + + def setUp(self): + self.db.execute("CREATE TABLE cs50(id INTEGER PRIMARY KEY, val TEXT)") + + def tearDown(self): + self.db.execute("DROP TABLE cs50") + + @classmethod + def tearDownClass(self): + self.db.execute("DROP TABLE IF EXISTS cs50") + +if __name__ == "__main__": + suite = unittest.TestSuite([ + unittest.TestLoader().loadTestsFromTestCase(SQLiteTests), + unittest.TestLoader().loadTestsFromTestCase(MySQLTests), + unittest.TestLoader().loadTestsFromTestCase(PostgresTests) + ]) + + unittest.TextTestRunner(verbosity=2).run(suite) From b63f5217f2e7d8cc7e4f83140105000ac1d511f9 Mon Sep 17 00:00:00 2001 From: "David J. Malan" <malan@harvard.edu> Date: Sun, 21 May 2017 00:35:14 -0400 Subject: [PATCH 15/35] added logging --- cs50/sql.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cs50/sql.py b/cs50/sql.py index 036e17c..93fd4e8 100644 --- a/cs50/sql.py +++ b/cs50/sql.py @@ -1,4 +1,5 @@ import datetime +import logging import re import sqlalchemy import sys @@ -16,6 +17,8 @@ def __init__(self, url, **kwargs): http://docs.sqlalchemy.org/en/latest/core/engines.html#sqlalchemy.create_engine http://docs.sqlalchemy.org/en/latest/dialects/index.html """ + logging.basicConfig(level=logging.DEBUG) + self.logger = logging.getLogger(__name__) try: self.engine = sqlalchemy.create_engine(url, **kwargs) except Exception as e: @@ -109,6 +112,9 @@ def process(value): # execute statement result = self.engine.execute(statement) + # log statement + self.logger.debug(statement) + # if SELECT (or INSERT with RETURNING), return result set as list of dict objects if re.search(r"^\s*SELECT\s+", statement, re.I): rows = result.fetchall() From bf2b82aa9a4193fb826adca19bf1a490f08b46db Mon Sep 17 00:00:00 2001 From: "David J. Malan" <malan@harvard.edu> Date: Sun, 21 May 2017 00:40:11 -0400 Subject: [PATCH 16/35] upped version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 09c4c5e..1b4c450 100644 --- a/setup.py +++ b/setup.py @@ -15,5 +15,5 @@ name="cs50", packages=["cs50"], url="https://github.com/cs50/python-cs50", - version="2.0.0" + version="2.1.0" ) From be16156ff37137f9cf999ab94b24279942fbfb6e Mon Sep 17 00:00:00 2001 From: "David J. Malan" <malan@harvard.edu> Date: Sun, 21 May 2017 00:49:52 -0400 Subject: [PATCH 17/35] fixed eprint for Python 2 --- cs50/cs50.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/cs50/cs50.py b/cs50/cs50.py index bae76a7..be6b402 100644 --- a/cs50/cs50.py +++ b/cs50/cs50.py @@ -21,14 +21,16 @@ def write(self, x): sys.stderr = flushfile(sys.stderr) sys.stdout = flushfile(sys.stdout) -def eprint(*objects, end="\n", sep=" "): +def eprint(*args, **kwargs): """ Print an error message to standard error, prefixing it with file name and line number from which method was called. """ - (frame, filename, lineno, function, code_context, index) = inspect.stack()[1] + end = kwargs.get("end", "\n") + sep = kwargs.get("sep", " ") + (filename, lineno) = inspect.stack()[1][1:3] print("{}:{}: ".format(filename, lineno), end="") - print(*objects, end=end, file=sys.stderr, sep=sep) + print(*args, end=end, file=sys.stderr, sep=sep) def get_char(): """Read a line of text from standard input and return the equivalent char.""" From f98662bd0f840294e7905a3e9616d397d14a73a5 Mon Sep 17 00:00:00 2001 From: "David J. Malan" <malan@harvard.edu> Date: Sun, 21 May 2017 13:19:43 -0400 Subject: [PATCH 18/35] reorganized directories --- {cs50 => src/cs50}/__init__.py | 0 src/cs50/__pycache__/__init__.cpython-34.pyc | Bin 0 -> 1121 bytes src/cs50/__pycache__/__init__.cpython-36.pyc | Bin 0 -> 1090 bytes src/cs50/__pycache__/cs50.cpython-34.pyc | Bin 0 -> 3748 bytes src/cs50/__pycache__/cs50.cpython-36.pyc | Bin 0 -> 4010 bytes src/cs50/__pycache__/sql.cpython-34.pyc | Bin 0 -> 3943 bytes {cs50 => src/cs50}/cs50.py | 0 {cs50 => src/cs50}/sql.py | 0 {test => tests}/python2.py | 0 {test => tests}/python3.py | 0 {test => tests}/sqltests.py | 0 11 files changed, 0 insertions(+), 0 deletions(-) rename {cs50 => src/cs50}/__init__.py (100%) create mode 100644 src/cs50/__pycache__/__init__.cpython-34.pyc create mode 100644 src/cs50/__pycache__/__init__.cpython-36.pyc create mode 100644 src/cs50/__pycache__/cs50.cpython-34.pyc create mode 100644 src/cs50/__pycache__/cs50.cpython-36.pyc create mode 100644 src/cs50/__pycache__/sql.cpython-34.pyc rename {cs50 => src/cs50}/cs50.py (100%) rename {cs50 => src/cs50}/sql.py (100%) rename {test => tests}/python2.py (100%) rename {test => tests}/python3.py (100%) rename {test => tests}/sqltests.py (100%) diff --git a/cs50/__init__.py b/src/cs50/__init__.py similarity index 100% rename from cs50/__init__.py rename to src/cs50/__init__.py diff --git a/src/cs50/__pycache__/__init__.cpython-34.pyc b/src/cs50/__pycache__/__init__.cpython-34.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c4a23a20dcccc32310bcd1ab62ed9833c61d226c GIT binary patch literal 1121 zcmZuw&2G~`5T3OiCrwgXRj3j@`GjPo*lAHgf(jM&fRIoS3Kw!&Irb)Yy7s!e8<kY$ z)V>U_z>6UH%BdF~04~frX&VHq?3dY{+4<(rKf9al-`|{1-z|V2u(lkOk8!K_2nqfO zaDdUBOkhMHu^_R5Tk!Es;y}`XYt%P@*>GjU$N@%R)_|D{%z-O_D*^=l41M<oox<RT zAo`>tPhxYvT{a5GxYa%aLsBF{L@p$?SER^yjmMfbaSidPVN^rESv@$JD=qT3c`2l3 zGJ1?WZ4cM-#*5WqKNx;^=ke$w;|os-Pp6UgBvaZG6OU(O8OengM};?L-dwT7JI{0~ z=Gwz&uX&uZeBot<(h=t@3EH*yRO?c`2*X6gDkv9dUj#x<!>8fOVVLD*km{V@nWOdR zY-tvhJUr;{AB6qGu>UMfqxdu{roE+~9=^m}RJi^!iHa$cqR=VkXvAR5#Z;9-2eHUk z&Yp*<IPYoE!z_Agp>n39GSX?!B((B=AJZT<#W&0R<4nK_ZuJo161za))B?E;mjFX- z$<Vh37p>cBeg)sI9L4ygYE9;x7g5eC$8gC8zOUieZYYJ&p?MQhniZL*6l+y=CRvft zTqJYO9vDg&0Z5lP<YMpdb_Q#vu?f*E6Rz2HxGzz_a5caTBX&XVtz^7>G{h`$PH<>* z8@^aTF0Fg*5gvxVJ&?vPPNt3R_MOVc!d1;>bZTU)GcG&Ez?LS=$}y?dH}{#!4WsFB z5#6S>gBPle0A!1F$Tr#hH?=i$->q7d8qbtgZMv!r%3Vs&=22Z<JT)oBd81Ss&vMJa zUA32z2}C*wCf2&H-KI;nochjhj%=JQ*+t#RF>R?_F`ltl*XnA+%nLP5tlX$9S&{tD XebqWz7Uz}uqE!o_OS)Fu+THmB{Co}! literal 0 HcmV?d00001 diff --git a/src/cs50/__pycache__/__init__.cpython-36.pyc b/src/cs50/__pycache__/__init__.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7eb8ca45c821c5d4b679a69e7913c6416a688c86 GIT binary patch literal 1090 zcmZuw!EV$r5Ve!+Zo1t<RdIn+!~rQtn=GvgQdOa%9uN{*2^9|HVr66RCan{vwpV4V z<<$NRzrc?m`O2xk02ju|wp|1h&3IzZyv&<%_O`bAzrTB*e>e#JL~G)Ke*~%yz)(an zM@hTl1XG8)-1&&!BI?o3HKHBnUb%_K@M-6?%e*Ueg)!uups;%b`DlED!G_r2=b3%K z6&IW%Q1uiHLx7ADhvLMgJ?c{L8Yh5HyVoe`PzTU_^Wb={v?$+|wUC<0^cev61CVwn z$khHQI{ENE;OS+-7l9Ii&Ql#orgR{t0WT&pm5U%tt6;){xneZ9D0D97I)Jn{Jj+?R z2#QMSlygR-euJKCU8{pQrXo{Oy@2>C5^@&r#Vf$LDC;QKCBF-Y=r2VZ7S%l79lh9% zNBi;UWt^wkSy9b~EzS@!fs2aQGih4Qm=u-HfunV!2^TX}3ms*mT!Focb8#`$VhAjT zYN1M|(>m4p&=Ojq-`6zGY`3l073PhOLDe>xC8qeyk%J{Vff+-eo$;lA+sSVzbd9GN zpBjHU=e$ZwW<2{H*@4`~{0iewib)lhDNn0-Ma&|^jUl9{3QY*K&TLMLijq>$IcE=S z1zj)*``E*mPyU{6v=*A#imlk{tv1z1;4HC5r}(TZAK7(@A6pi1N4ZnFUmc8=PP=~y z9&mHV(n7&ybYbA3XIyBV>9qx^#8u~9ZrT7>+RBV)2{$|UeC38c+2kU<WwHYsst*R? zK0E`^f7q;rhj<)zjZcULC&cv0sw3bJ2sxjpO`qYdM+jUxLZk(k53RXtd+XU+r2SuQ zxlK3yo`?HhvlsRf7cQ6_z?t#vFiclWPFbd#W3yo|4YAWS-L$S*MgIqH{KK|CukC-V Nd@w%t9pBmB`2!&k6t@5X literal 0 HcmV?d00001 diff --git a/src/cs50/__pycache__/cs50.cpython-34.pyc b/src/cs50/__pycache__/cs50.cpython-34.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dd8d33b93ba87f8c14dcf7963161fb808172e8cb GIT binary patch literal 3748 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 E0FF{prT_o{ literal 0 HcmV?d00001 diff --git a/src/cs50/__pycache__/cs50.cpython-36.pyc b/src/cs50/__pycache__/cs50.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b4444811a29651f884b781ed251824821529a236 GIT binary patch literal 4010 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 X6n^gV3bu@HbuoG=T8-AQhSmQ7zgoyZ literal 0 HcmV?d00001 diff --git a/src/cs50/__pycache__/sql.cpython-34.pyc b/src/cs50/__pycache__/sql.cpython-34.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8e567319c0fdc2669c45658d3518ccd7543a53ca GIT binary patch literal 3943 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 literal 0 HcmV?d00001 diff --git a/cs50/cs50.py b/src/cs50/cs50.py similarity index 100% rename from cs50/cs50.py rename to src/cs50/cs50.py diff --git a/cs50/sql.py b/src/cs50/sql.py similarity index 100% rename from cs50/sql.py rename to src/cs50/sql.py diff --git a/test/python2.py b/tests/python2.py similarity index 100% rename from test/python2.py rename to tests/python2.py diff --git a/test/python3.py b/tests/python3.py similarity index 100% rename from test/python3.py rename to tests/python3.py diff --git a/test/sqltests.py b/tests/sqltests.py similarity index 100% rename from test/sqltests.py rename to tests/sqltests.py From cc1f84042465312af705fe41b2cae87e4ecac762 Mon Sep 17 00:00:00 2001 From: "David J. Malan" <malan@harvard.edu> Date: Wed, 24 May 2017 21:06:02 -0400 Subject: [PATCH 19/35] fixed naming conventions, fixed exception handling --- src/cs50/sql.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/cs50/sql.py b/src/cs50/sql.py index 93fd4e8..7af6f39 100644 --- a/src/cs50/sql.py +++ b/src/cs50/sql.py @@ -1,4 +1,5 @@ import datetime +import importlib import logging import re import sqlalchemy @@ -22,8 +23,9 @@ def __init__(self, url, **kwargs): try: self.engine = sqlalchemy.create_engine(url, **kwargs) except Exception as e: - e.__cause__ = None - raise RuntimeError(e) + e_ = RuntimeError(e) # else Python 3 prints warnings' tracebacks + e_.__cause__ = None + raise e_ def execute(self, text, **params): """ @@ -141,6 +143,6 @@ def process(value): # else raise exception except Exception as e: - _e = RuntimeError(e) # else Python 3 prints warnings' tracebacks - _e.__cause__ = None - raise _e + e_ = RuntimeError(e) # else Python 3 prints warnings' tracebacks + e_.__cause__ = None + raise e_ From a115e9ae0a24721027afec4368c08bea0e7a012b Mon Sep 17 00:00:00 2001 From: "David J. Malan" <malan@harvard.edu> Date: Wed, 24 May 2017 21:11:29 -0400 Subject: [PATCH 20/35] no longer reraising RuntimeErrors --- src/cs50/sql.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/cs50/sql.py b/src/cs50/sql.py index 7af6f39..6649d13 100644 --- a/src/cs50/sql.py +++ b/src/cs50/sql.py @@ -23,9 +23,8 @@ def __init__(self, url, **kwargs): try: self.engine = sqlalchemy.create_engine(url, **kwargs) except Exception as e: - e_ = RuntimeError(e) # else Python 3 prints warnings' tracebacks - e_.__cause__ = None - raise e_ + e.__cause__ = None # else Python 3 prints warnings' tracebacks + raise e def execute(self, text, **params): """ @@ -143,6 +142,5 @@ def process(value): # else raise exception except Exception as e: - e_ = RuntimeError(e) # else Python 3 prints warnings' tracebacks - e_.__cause__ = None - raise e_ + e.__cause__ = None # else Python 3 prints warnings' tracebacks + raise e From 0032a54b67c0514834ea440aa10fdefb86003674 Mon Sep 17 00:00:00 2001 From: "David J. Malan" <malan@harvard.edu> Date: Wed, 24 May 2017 21:11:42 -0400 Subject: [PATCH 21/35] upped version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 1b4c450..2342a33 100644 --- a/setup.py +++ b/setup.py @@ -15,5 +15,5 @@ name="cs50", packages=["cs50"], url="https://github.com/cs50/python-cs50", - version="2.1.0" + version="2.2.0" ) From 7ef9d6d4edc2a5b197a89eaeb258964b8ee677d6 Mon Sep 17 00:00:00 2001 From: "David J. Malan" <malan@harvard.edu> Date: Wed, 24 May 2017 21:17:22 -0400 Subject: [PATCH 22/35] no longer reraising exceptions --- src/cs50/sql.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/cs50/sql.py b/src/cs50/sql.py index 6649d13..df5a20c 100644 --- a/src/cs50/sql.py +++ b/src/cs50/sql.py @@ -20,11 +20,7 @@ def __init__(self, url, **kwargs): """ logging.basicConfig(level=logging.DEBUG) self.logger = logging.getLogger(__name__) - try: - self.engine = sqlalchemy.create_engine(url, **kwargs) - except Exception as e: - e.__cause__ = None # else Python 3 prints warnings' tracebacks - raise e + self.engine = sqlalchemy.create_engine(url, **kwargs) def execute(self, text, **params): """ @@ -139,8 +135,3 @@ def process(value): # if constraint violated, return None except sqlalchemy.exc.IntegrityError: return None - - # else raise exception - except Exception as e: - e.__cause__ = None # else Python 3 prints warnings' tracebacks - raise e From 22306093bae78b05ebf7a8e3b026af03d1f3c537 Mon Sep 17 00:00:00 2001 From: "David J. Malan" <malan@harvard.edu> Date: Wed, 24 May 2017 21:23:02 -0400 Subject: [PATCH 23/35] added comments --- src/cs50/sql.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/cs50/sql.py b/src/cs50/sql.py index df5a20c..bfa79f0 100644 --- a/src/cs50/sql.py +++ b/src/cs50/sql.py @@ -18,8 +18,12 @@ def __init__(self, url, **kwargs): http://docs.sqlalchemy.org/en/latest/core/engines.html#sqlalchemy.create_engine http://docs.sqlalchemy.org/en/latest/dialects/index.html """ + + # log statements to standard error logging.basicConfig(level=logging.DEBUG) self.logger = logging.getLogger(__name__) + + # create engine, raising exception if back end's module not installed self.engine = sqlalchemy.create_engine(url, **kwargs) def execute(self, text, **params): From 41f1027e8af3c0cfbf43f24b5bc44a8384cfa171 Mon Sep 17 00:00:00 2001 From: Kareem Zidane <kzidane@cs50.harvard.edu> Date: Sat, 27 May 2017 09:56:49 +0200 Subject: [PATCH 24/35] ignored *.pyc and *.db --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index f75ca9b..5a13495 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ !.gitignore !.travis.yml dist/ +*.db *.egg-info/ +*.pyc From a1bbaab4704985c3c3f3d79e5653a4fc4b873e4c Mon Sep 17 00:00:00 2001 From: Kareem Zidane <kzidane@cs50.harvard.edu> Date: Sat, 27 May 2017 10:06:24 +0200 Subject: [PATCH 25/35] exiting with error code if sql test failed --- tests/sqltests.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/sqltests.py b/tests/sqltests.py index d2204a1..6cddabb 100644 --- a/tests/sqltests.py +++ b/tests/sqltests.py @@ -1,3 +1,4 @@ +import sys import unittest from cs50.sql import SQL @@ -126,4 +127,4 @@ def tearDownClass(self): unittest.TestLoader().loadTestsFromTestCase(PostgresTests) ]) - unittest.TextTestRunner(verbosity=2).run(suite) + sys.exit(not unittest.TextTestRunner(verbosity=2).run(suite).wasSuccessful()) From f91a93ef359ec61c83743a5ac8a72a02550df693 Mon Sep 17 00:00:00 2001 From: Kareem Zidane <kzidane@cs50.harvard.edu> Date: Sat, 27 May 2017 10:36:23 +0200 Subject: [PATCH 26/35] suppressed unknown table in mysql's tearDownClass --- tests/sqltests.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/sqltests.py b/tests/sqltests.py index 6cddabb..7a569ad 100644 --- a/tests/sqltests.py +++ b/tests/sqltests.py @@ -84,7 +84,13 @@ def tearDown(self): @classmethod def tearDownClass(self): - self.db.execute("DROP TABLE IF EXISTS cs50") + try: + self.db.execute("DROP TABLE IF EXISTS cs50") + except RuntimeError as e: + + # suppress "unknown table" + if not str(e).startswith("(1051L"): + raise e class PostgresTests(SQLTests): @classmethod From d3c25fce4a0ca48b360ba3016a522f0f6109df99 Mon Sep 17 00:00:00 2001 From: Kareem Zidane <kzidane@cs50.harvard.edu> Date: Sat, 27 May 2017 10:36:59 +0200 Subject: [PATCH 27/35] dropped postgres password for travis --- tests/sqltests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/sqltests.py b/tests/sqltests.py index 7a569ad..cb8bec5 100644 --- a/tests/sqltests.py +++ b/tests/sqltests.py @@ -95,7 +95,7 @@ def tearDownClass(self): class PostgresTests(SQLTests): @classmethod def setUpClass(self): - self.db = SQL("postgresql://postgres:postgres@localhost/cs50_sql_tests") + self.db = SQL("postgresql://postgres@localhost/cs50_sql_tests") def setUp(self): self.db.execute("CREATE TABLE cs50 (id SERIAL PRIMARY KEY, val VARCHAR(16))") From c3253f9ee3c508d709906ffaff854d28e20a2242 Mon Sep 17 00:00:00 2001 From: Kareem Zidane <kzidane@cs50.harvard.edu> Date: Sat, 27 May 2017 10:51:20 +0200 Subject: [PATCH 28/35] running sql tests against python 2 and 3 on travis --- .travis.yml | 17 ++++++++++++++--- tests/sqltests.py | 6 +++--- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index f097728..287bcab 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,20 @@ language: python -python: '3.4' +python: +- '2.7' +- '3.4' branches: except: "/^v\\d/" -install: true -script: true +services: +- mysql +- postgresql +install: +- python setup.py install +- pip install mysqlclient +- pip install psycopg2 +before_script: +- mysql -e 'CREATE DATABASE IF NOT EXISTS test;' +- psql -c 'create database test;' -U postgres +script: python test/sqltests.py deploy: - provider: script script: 'curl --fail --data "{ \"tag_name\": \"v$(python setup.py --version)\", diff --git a/tests/sqltests.py b/tests/sqltests.py index cb8bec5..3cf5311 100644 --- a/tests/sqltests.py +++ b/tests/sqltests.py @@ -74,7 +74,7 @@ def test_update_returns_affected_rows(self): class MySQLTests(SQLTests): @classmethod def setUpClass(self): - self.db = SQL("mysql://root@localhost/cs50_sql_tests") + self.db = SQL("mysql://root@localhost/test") def setUp(self): self.db.execute("CREATE TABLE cs50 (id INTEGER NOT NULL AUTO_INCREMENT, val VARCHAR(16), PRIMARY KEY (id))") @@ -95,7 +95,7 @@ def tearDownClass(self): class PostgresTests(SQLTests): @classmethod def setUpClass(self): - self.db = SQL("postgresql://postgres@localhost/cs50_sql_tests") + self.db = SQL("postgresql://postgres@localhost/test") def setUp(self): self.db.execute("CREATE TABLE cs50 (id SERIAL PRIMARY KEY, val VARCHAR(16))") @@ -114,7 +114,7 @@ def test_insert_returns_last_row_id(self): class SQLiteTests(SQLTests): @classmethod def setUpClass(self): - self.db = SQL("sqlite:///cs50_sql_tests.db") + self.db = SQL("sqlite:///test.db") def setUp(self): self.db.execute("CREATE TABLE cs50(id INTEGER PRIMARY KEY, val TEXT)") From 2a07bc877fc0245347bfce1f7f67126fef602f89 Mon Sep 17 00:00:00 2001 From: Kareem Zidane <kzidane@cs50.harvard.edu> Date: Sat, 27 May 2017 11:17:11 +0200 Subject: [PATCH 29/35] removed __pycache__ --- src/cs50/__pycache__/__init__.cpython-34.pyc | Bin 1121 -> 0 bytes src/cs50/__pycache__/__init__.cpython-36.pyc | Bin 1090 -> 0 bytes src/cs50/__pycache__/cs50.cpython-34.pyc | Bin 3748 -> 0 bytes src/cs50/__pycache__/cs50.cpython-36.pyc | Bin 4010 -> 0 bytes src/cs50/__pycache__/sql.cpython-34.pyc | Bin 3943 -> 0 bytes 5 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/cs50/__pycache__/__init__.cpython-34.pyc delete mode 100644 src/cs50/__pycache__/__init__.cpython-36.pyc delete mode 100644 src/cs50/__pycache__/cs50.cpython-34.pyc delete mode 100644 src/cs50/__pycache__/cs50.cpython-36.pyc delete mode 100644 src/cs50/__pycache__/sql.cpython-34.pyc diff --git a/src/cs50/__pycache__/__init__.cpython-34.pyc b/src/cs50/__pycache__/__init__.cpython-34.pyc deleted file mode 100644 index c4a23a20dcccc32310bcd1ab62ed9833c61d226c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1121 zcmZuw&2G~`5T3OiCrwgXRj3j@`GjPo*lAHgf(jM&fRIoS3Kw!&Irb)Yy7s!e8<kY$ z)V>U_z>6UH%BdF~04~frX&VHq?3dY{+4<(rKf9al-`|{1-z|V2u(lkOk8!K_2nqfO zaDdUBOkhMHu^_R5Tk!Es;y}`XYt%P@*>GjU$N@%R)_|D{%z-O_D*^=l41M<oox<RT zAo`>tPhxYvT{a5GxYa%aLsBF{L@p$?SER^yjmMfbaSidPVN^rESv@$JD=qT3c`2l3 zGJ1?WZ4cM-#*5WqKNx;^=ke$w;|os-Pp6UgBvaZG6OU(O8OengM};?L-dwT7JI{0~ z=Gwz&uX&uZeBot<(h=t@3EH*yRO?c`2*X6gDkv9dUj#x<!>8fOVVLD*km{V@nWOdR zY-tvhJUr;{AB6qGu>UMfqxdu{roE+~9=^m}RJi^!iHa$cqR=VkXvAR5#Z;9-2eHUk z&Yp*<IPYoE!z_Agp>n39GSX?!B((B=AJZT<#W&0R<4nK_ZuJo161za))B?E;mjFX- z$<Vh37p>cBeg)sI9L4ygYE9;x7g5eC$8gC8zOUieZYYJ&p?MQhniZL*6l+y=CRvft zTqJYO9vDg&0Z5lP<YMpdb_Q#vu?f*E6Rz2HxGzz_a5caTBX&XVtz^7>G{h`$PH<>* z8@^aTF0Fg*5gvxVJ&?vPPNt3R_MOVc!d1;>bZTU)GcG&Ez?LS=$}y?dH}{#!4WsFB z5#6S>gBPle0A!1F$Tr#hH?=i$->q7d8qbtgZMv!r%3Vs&=22Z<JT)oBd81Ss&vMJa zUA32z2}C*wCf2&H-KI;nochjhj%=JQ*+t#RF>R?_F`ltl*XnA+%nLP5tlX$9S&{tD XebqWz7Uz}uqE!o_OS)Fu+THmB{Co}! diff --git a/src/cs50/__pycache__/__init__.cpython-36.pyc b/src/cs50/__pycache__/__init__.cpython-36.pyc deleted file mode 100644 index 7eb8ca45c821c5d4b679a69e7913c6416a688c86..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1090 zcmZuw!EV$r5Ve!+Zo1t<RdIn+!~rQtn=GvgQdOa%9uN{*2^9|HVr66RCan{vwpV4V z<<$NRzrc?m`O2xk02ju|wp|1h&3IzZyv&<%_O`bAzrTB*e>e#JL~G)Ke*~%yz)(an zM@hTl1XG8)-1&&!BI?o3HKHBnUb%_K@M-6?%e*Ueg)!uups;%b`DlED!G_r2=b3%K z6&IW%Q1uiHLx7ADhvLMgJ?c{L8Yh5HyVoe`PzTU_^Wb={v?$+|wUC<0^cev61CVwn z$khHQI{ENE;OS+-7l9Ii&Ql#orgR{t0WT&pm5U%tt6;){xneZ9D0D97I)Jn{Jj+?R z2#QMSlygR-euJKCU8{pQrXo{Oy@2>C5^@&r#Vf$LDC;QKCBF-Y=r2VZ7S%l79lh9% zNBi;UWt^wkSy9b~EzS@!fs2aQGih4Qm=u-HfunV!2^TX}3ms*mT!Focb8#`$VhAjT zYN1M|(>m4p&=Ojq-`6zGY`3l073PhOLDe>xC8qeyk%J{Vff+-eo$;lA+sSVzbd9GN zpBjHU=e$ZwW<2{H*@4`~{0iewib)lhDNn0-Ma&|^jUl9{3QY*K&TLMLijq>$IcE=S z1zj)*``E*mPyU{6v=*A#imlk{tv1z1;4HC5r}(TZAK7(@A6pi1N4ZnFUmc8=PP=~y z9&mHV(n7&ybYbA3XIyBV>9qx^#8u~9ZrT7>+RBV)2{$|UeC38c+2kU<WwHYsst*R? zK0E`^f7q;rhj<)zjZcULC&cv0sw3bJ2sxjpO`qYdM+jUxLZk(k53RXtd+XU+r2SuQ zxlK3yo`?HhvlsRf7cQ6_z?t#vFiclWPFbd#W3yo|4YAWS-L$S*MgIqH{KK|CukC-V Nd@w%t9pBmB`2!&k6t@5X diff --git a/src/cs50/__pycache__/cs50.cpython-34.pyc b/src/cs50/__pycache__/cs50.cpython-34.pyc deleted file mode 100644 index dd8d33b93ba87f8c14dcf7963161fb808172e8cb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3748 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 E0FF{prT_o{ diff --git a/src/cs50/__pycache__/cs50.cpython-36.pyc b/src/cs50/__pycache__/cs50.cpython-36.pyc deleted file mode 100644 index b4444811a29651f884b781ed251824821529a236..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4010 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 X6n^gV3bu@HbuoG=T8-AQhSmQ7zgoyZ diff --git a/src/cs50/__pycache__/sql.cpython-34.pyc b/src/cs50/__pycache__/sql.cpython-34.pyc deleted file mode 100644 index 8e567319c0fdc2669c45658d3518ccd7543a53ca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3943 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 From 77dbb658ad1bb942954b8159e2cd75c227dde66f Mon Sep 17 00:00:00 2001 From: Kareem Zidane <kzidane@cs50.harvard.edu> Date: Sat, 27 May 2017 11:17:31 +0200 Subject: [PATCH 30/35] fixed package path --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 2342a33..6b7fce7 100644 --- a/setup.py +++ b/setup.py @@ -13,6 +13,7 @@ install_requires=["SQLAlchemy"], keywords="cs50", name="cs50", + package_dir={"": "src"}, packages=["cs50"], url="https://github.com/cs50/python-cs50", version="2.2.0" From 4bc77abd3be2c20dc166e197f8cabf012e869948 Mon Sep 17 00:00:00 2001 From: Kareem Zidane <kzidane@cs50.harvard.edu> Date: Sat, 27 May 2017 11:21:23 +0200 Subject: [PATCH 31/35] fixed sqltests.py path --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 287bcab..a20df42 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,7 +14,7 @@ install: before_script: - mysql -e 'CREATE DATABASE IF NOT EXISTS test;' - psql -c 'create database test;' -U postgres -script: python test/sqltests.py +script: python tests/sqltests.py deploy: - provider: script script: 'curl --fail --data "{ \"tag_name\": \"v$(python setup.py --version)\", From a45a3ec575b7fc0cf421d3c62ccabaf7663735f1 Mon Sep 17 00:00:00 2001 From: Kareem Zidane <kzidane@cs50.harvard.edu> Date: Sat, 27 May 2017 11:55:19 +0200 Subject: [PATCH 32/35] fixed unknown table suppression in sql tests --- tests/sqltests.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/sqltests.py b/tests/sqltests.py index 3cf5311..26778dc 100644 --- a/tests/sqltests.py +++ b/tests/sqltests.py @@ -1,6 +1,7 @@ import sys import unittest from cs50.sql import SQL +import warnings class SQLTests(unittest.TestCase): def test_delete_returns_affected_rows(self): @@ -86,10 +87,9 @@ def tearDown(self): def tearDownClass(self): try: self.db.execute("DROP TABLE IF EXISTS cs50") - except RuntimeError as e: - + except Warning as e: # suppress "unknown table" - if not str(e).startswith("(1051L"): + if not str(e).startswith("(1051"): raise e class PostgresTests(SQLTests): From f7cf36992f9805ad6c37854bbcf814623161cbb2 Mon Sep 17 00:00:00 2001 From: Kareem Zidane <kzidane@cs50.harvard.edu> Date: Sat, 27 May 2017 12:03:29 +0200 Subject: [PATCH 33/35] simplified sqltests --- tests/sqltests.py | 34 ++++++++-------------------------- 1 file changed, 8 insertions(+), 26 deletions(-) diff --git a/tests/sqltests.py b/tests/sqltests.py index 26778dc..0ce8af0 100644 --- a/tests/sqltests.py +++ b/tests/sqltests.py @@ -72,14 +72,6 @@ def test_update_returns_affected_rows(self): self.assertEqual(self.db.execute("UPDATE cs50 SET val = 'foo' WHERE id > 1"), 2) self.assertEqual(self.db.execute("UPDATE cs50 SET val = 'foo' WHERE id = -50"), 0) -class MySQLTests(SQLTests): - @classmethod - def setUpClass(self): - self.db = SQL("mysql://root@localhost/test") - - def setUp(self): - self.db.execute("CREATE TABLE cs50 (id INTEGER NOT NULL AUTO_INCREMENT, val VARCHAR(16), PRIMARY KEY (id))") - def tearDown(self): self.db.execute("DROP TABLE cs50") @@ -92,24 +84,21 @@ def tearDownClass(self): if not str(e).startswith("(1051"): raise e -class PostgresTests(SQLTests): +class MySQLTests(SQLTests): @classmethod def setUpClass(self): - self.db = SQL("postgresql://postgres@localhost/test") + self.db = SQL("mysql://root@localhost/test") def setUp(self): - self.db.execute("CREATE TABLE cs50 (id SERIAL PRIMARY KEY, val VARCHAR(16))") - - def tearDown(self): - self.db.execute("DROP TABLE cs50") + self.db.execute("CREATE TABLE cs50 (id INTEGER NOT NULL AUTO_INCREMENT, val VARCHAR(16), PRIMARY KEY (id))") +class PostgresTests(SQLTests): @classmethod - def tearDownClass(self): - self.db.execute("DROP TABLE IF EXISTS cs50") + def setUpClass(self): + self.db = SQL("postgresql://postgres@localhost/test") - def test_insert_returns_last_row_id(self): - self.assertEqual(self.db.execute("INSERT INTO cs50(val) VALUES('foo')"), 1) - self.assertEqual(self.db.execute("INSERT INTO cs50(val) VALUES('bar')"), 2) + def setUp(self): + self.db.execute("CREATE TABLE cs50 (id SERIAL PRIMARY KEY, val VARCHAR(16))") class SQLiteTests(SQLTests): @classmethod @@ -119,13 +108,6 @@ def setUpClass(self): def setUp(self): self.db.execute("CREATE TABLE cs50(id INTEGER PRIMARY KEY, val TEXT)") - def tearDown(self): - self.db.execute("DROP TABLE cs50") - - @classmethod - def tearDownClass(self): - self.db.execute("DROP TABLE IF EXISTS cs50") - if __name__ == "__main__": suite = unittest.TestSuite([ unittest.TestLoader().loadTestsFromTestCase(SQLiteTests), From 5e2b03eee32a6c48f645cb586daabbb1715d7be7 Mon Sep 17 00:00:00 2001 From: Kareem Zidane <kzidane@cs50.harvard.edu> Date: Sat, 27 May 2017 14:50:00 +0200 Subject: [PATCH 34/35] added test for multi-insert statements --- tests/sqltests.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/sqltests.py b/tests/sqltests.py index 0ce8af0..8e518b4 100644 --- a/tests/sqltests.py +++ b/tests/sqltests.py @@ -1,9 +1,12 @@ +from cs50.sql import SQL import sys import unittest -from cs50.sql import SQL import warnings class SQLTests(unittest.TestCase): + def multi_inserts_enabled(self): + return True + def test_delete_returns_affected_rows(self): rows = [ {"id": 1, "val": "foo"}, @@ -24,6 +27,8 @@ def test_delete_returns_affected_rows(self): def test_insert_returns_last_row_id(self): self.assertEqual(self.db.execute("INSERT INTO cs50(val) VALUES('foo')"), 1) self.assertEqual(self.db.execute("INSERT INTO cs50(val) VALUES('bar')"), 2) + if self.multi_inserts_enabled(): + self.assertEqual(self.db.execute("INSERT INTO cs50(val) VALUES('baz'); INSERT INTO cs50(val) VALUES('qux')"), 4) def test_select_all(self): self.assertEqual(self.db.execute("SELECT * FROM cs50"), []) @@ -108,6 +113,9 @@ def setUpClass(self): def setUp(self): self.db.execute("CREATE TABLE cs50(id INTEGER PRIMARY KEY, val TEXT)") + def multi_inserts_enabled(self): + return False + if __name__ == "__main__": suite = unittest.TestSuite([ unittest.TestLoader().loadTestsFromTestCase(SQLiteTests), From da85752145d5937234959fd3d8e6479ca5e56877 Mon Sep 17 00:00:00 2001 From: Kareem Zidane <kzidane@cs50.harvard.edu> Date: Sat, 27 May 2017 15:08:34 +0200 Subject: [PATCH 35/35] decreased version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 6b7fce7..8440a86 100644 --- a/setup.py +++ b/setup.py @@ -16,5 +16,5 @@ package_dir={"": "src"}, packages=["cs50"], url="https://github.com/cs50/python-cs50", - version="2.2.0" + version="2.0.0" )