From 3b855bfea8a809ea400e6321714f99feba0d84b0 Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Sat, 27 Nov 2021 14:59:52 +0100 Subject: [PATCH 01/16] init. --- .gitattributes | 1 - .gitignore | 2 +- setup.cfg | 4 ++++ tox.ini | 8 +++----- 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index c13bfc4..0000000 --- a/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -src/pytask_environment/_version.py export-subst diff --git a/.gitignore b/.gitignore index ef7ce74..1d68ee7 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,4 @@ __pycache__ build dist -src/pytask_latex/_version.py +src/pytask_environment/_version.py diff --git a/setup.cfg b/setup.cfg index 2f4cd0a..77083bd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -42,3 +42,7 @@ where = src [options.entry_points] pytask = pytask_environment = pytask_environment.plugin + +[check-manifest] +ignore = + src/pytask_environment/_version.py diff --git a/tox.ini b/tox.ini index 792ea36..b309475 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = pytest, pre-commit +envlist = pytest skipsdist = True skip_missing_interpreters = True @@ -20,10 +20,6 @@ commands = pip install --no-deps . pytest {posargs} -[testenv:pre-commit] -deps = pre-commit -commands = pre-commit run --all-files - [doc8] ignore = D002, D004 max-line-length = 89 @@ -46,6 +42,8 @@ addopts = --doctest-modules filterwarnings = ignore: the imp module is deprecated in favour of importlib ignore: Using or importing the ABCs from 'collections' instead of from + ignore: The parser module is deprecated and will + ignore: The symbol module is deprecated markers = wip: Tests that are work-in-progress. unit: Flag for unit tests which target mainly a single function. From 426ce7b2845803d2627c04e81ac776311e239154 Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Tue, 25 Jan 2022 02:06:36 +0100 Subject: [PATCH 02/16] Replace input prompts with configuration values. --- CHANGES.rst | 14 ++++++- README.rst | 7 ---- src/pytask_environment/collect.py | 69 ++++++++++++++++++------------- src/pytask_environment/config.py | 43 +++++++++++++++++++ src/pytask_environment/plugin.py | 2 + tests/test_collect.py | 67 +++++++++++++----------------- 6 files changed, 127 insertions(+), 75 deletions(-) create mode 100644 src/pytask_environment/config.py diff --git a/CHANGES.rst b/CHANGES.rst index b21de40..c259e20 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -7,12 +7,24 @@ reverse chronological order. Releases follow `semantic versioning `_. -0.0.5 - 2021-07-23 +0.1.0 - 2022-01-25 +------------------ + +- :gh:`10` replaces the input prompts with configuration values and flags. + + +0.0.6 - 2021-07-23 ------------------ - :gh:`8` replaces versioneer with setuptools-scm. +0.0.5 - 2021-07-23 +------------------ + +- :gh:`7` adds some pre-commit updates. + + 0.0.4 - 2021-03-05 ------------------ diff --git a/README.rst b/README.rst index 84fa95c..218ed12 100644 --- a/README.rst +++ b/README.rst @@ -61,13 +61,6 @@ If the user attempts to build the project and the Python version has been cached database in a previous run, an invocation with a different environment will produce the following command line output. -.. code-block:: console - - $ pytask build - Your Python environment seems to have changed. The Python version has - changed. The path to the Python executable has changed. Do you want - to continue with the current environment? [y/N]: - Future development ------------------ diff --git a/src/pytask_environment/collect.py b/src/pytask_environment/collect.py index 34375d7..9da9028 100644 --- a/src/pytask_environment/collect.py +++ b/src/pytask_environment/collect.py @@ -1,15 +1,21 @@ import sys -import click from _pytask.config import hookimpl -from _pytask.exceptions import CollectionError +from _pytask.console import console from pony import orm from pytask_environment.database import Environment +_ERROR_MSG = """\ +Aborted execution due to a bad state of the environment. Either switch to the correct +environment or update the information on the environment using the --update-environment +flag. +""" + + @hookimpl(trylast=True) -def pytask_log_session_header(): - """Use the entry-point to implement an early exit. +def pytask_log_session_header(session) -> None: + """Check environment and python version. The solution is hacky. Exploit the first entry-point in the build process after the database is created. @@ -18,37 +24,44 @@ def pytask_log_session_header(): the user whether she wants to proceed. """ - same_version, same_path = have_version_or_path_changed( - "python", sys.version, sys.executable - ) - if not same_version or not same_path: - message = "\nYour Python environment seems to have changed." - message += " The Python version has changed." if not same_version else "" - message += ( - " The path to the Python executable has changed." if not same_path else "" - ) - message += " Do you want to continue with the current environment?" - - if click.confirm(message): - create_or_update_state("python", sys.version, sys.executable) - else: - raise CollectionError + __tracebackhide__ = True + + package = retrieve_package("python") + + same_version = True if package is None else sys.version == package.version + same_path = True if package is None else sys.executable == package.path + + msg = "" + if not same_version and session.config["check_python_version"]: + msg += " The Python version has changed " + if package is not None: + msg += f"from\n\n{package.version}\n\n" + msg += f"to\n\n{sys.version}\n\n" + if not same_path and session.config["check_environment"]: + msg += "The path to the Python interpreter has changed " + if package is not None: + msg += f"from\n\n{package.path}\n\n" + msg += f"to\n\n{sys.executable}." + + if msg: + msg = "Your Python environment has changed." + msg + + if session.config["update_environment"] or package is None: + console.print("Update the information in the database.") + create_or_update_state("python", sys.version, sys.executable) + else: + console.print() + raise Exception(msg + "\n\n" + _ERROR_MSG) from None @orm.db_session -def have_version_or_path_changed(name, version, path): +def retrieve_package(name): """Return booleans indicating whether the version or path of a package changed.""" try: package = Environment[name] except orm.ObjectNotFound: - Environment(name=name, version=version, path=path) - same_version = True - same_path = True - else: - same_version = package.version == version - same_path = package.path == path - - return same_version, same_path + package = None + return package @orm.db_session diff --git a/src/pytask_environment/config.py b/src/pytask_environment/config.py new file mode 100644 index 0000000..ee9a057 --- /dev/null +++ b/src/pytask_environment/config.py @@ -0,0 +1,43 @@ +import click +from _pytask.config import hookimpl +from _pytask.shared import convert_truthy_or_falsy_to_bool +from _pytask.shared import get_first_non_none_value + + +@hookimpl +def pytask_extend_command_line_interface(cli): + cli.commands["build"].params.append( + click.Option( + ["--update-environment"], + is_flag=True, + default=None, + help="Update the information on the environment stored in the database.", + ) + ) + + +@hookimpl +def pytask_parse_config(config, config_from_file, config_from_cli): + config["check_python_version"] = get_first_non_none_value( + config_from_cli, + config_from_file, + key="check_python_version", + default=True, + callback=convert_truthy_or_falsy_to_bool, + ) + + config["check_environment"] = get_first_non_none_value( + config_from_cli, + config_from_file, + key="check_environment", + default=True, + callback=convert_truthy_or_falsy_to_bool, + ) + + config["update_environment"] = get_first_non_none_value( + config_from_cli, + config_from_file, + key="update_environment", + default=False, + callback=convert_truthy_or_falsy_to_bool, + ) diff --git a/src/pytask_environment/plugin.py b/src/pytask_environment/plugin.py index 705eb92..ae3441b 100644 --- a/src/pytask_environment/plugin.py +++ b/src/pytask_environment/plugin.py @@ -1,6 +1,7 @@ """Entry-point for the plugin.""" from _pytask.config import hookimpl from pytask_environment import collect +from pytask_environment import config from pytask_environment import database @@ -8,4 +9,5 @@ def pytask_add_hooks(pm): """Register some plugins.""" pm.register(collect) + pm.register(config) pm.register(database) diff --git a/tests/test_collect.py b/tests/test_collect.py index 560a043..eee9f73 100644 --- a/tests/test_collect.py +++ b/tests/test_collect.py @@ -12,32 +12,25 @@ @pytest.mark.end_to_end def test_existence_of_python_executable_in_db(tmp_path, runner): """Test that the Python executable is stored in the database.""" - source = """ - import pytask - - def task_dummy(): - pass - """ task_path = tmp_path.joinpath("task_dummy.py") - task_path.write_text(textwrap.dedent(source)) + task_path.write_text(textwrap.dedent("def task_dummy(): pass")) os.chdir(tmp_path) result = runner.invoke(cli) assert result.exit_code == 0 - orm.db_session.__enter__() + with orm.db_session: - create_database( - "sqlite", tmp_path.joinpath(".pytask.sqlite3").as_posix(), True, False - ) + create_database( + "sqlite", tmp_path.joinpath(".pytask.sqlite3").as_posix(), True, False + ) + + python = Environment["python"] - python = Environment["python"] assert python.version == sys.version assert python.path == sys.executable - orm.db_session.__exit__() - @pytest.mark.skipif( sys.platform == "win32" and sys.version_info[:2] == (3, 6), @@ -61,51 +54,47 @@ def test_prompt_when_python_version_has_changed(monkeypatch, tmp_path, runner): "[MSC v.1916 64 bit (AMD64)]" ) - source = """ - import pytask - - def task_dummy(): - pass - """ + source = "def task_dummy(): pass" task_path = tmp_path.joinpath("task_dummy.py") task_path.write_text(textwrap.dedent(source)) os.chdir(tmp_path) + + # Run without knowing the python version and without updating the environment. result = runner.invoke(cli) + assert result.exit_code == 1 + # Run with updating the environment. + result = runner.invoke(cli, ["--update-environment"]) assert result.exit_code == 0 + # Run with a fake version and not updating the environment. monkeypatch.setattr("pytask_environment.collect.sys.version", fake_version) - result = runner.invoke(cli, input="N") - assert result.exit_code == 3 - - orm.db_session.__enter__() + result = runner.invoke(cli) + assert result.exit_code == 1 - create_database( - "sqlite", tmp_path.joinpath(".pytask.sqlite3").as_posix(), True, False - ) + with orm.db_session: + create_database( + "sqlite", tmp_path.joinpath(".pytask.sqlite3").as_posix(), True, False + ) + python = Environment["python"] - python = Environment["python"] assert python.version == real_python_version assert python.path == real_python_executable - orm.db_session.__exit__() - + # Run with a fake version and updating the environment. monkeypatch.setattr("pytask_environment.collect.sys.version", fake_version) monkeypatch.setattr("pytask_environment.collect.sys.executable", "new_path") - result = runner.invoke(cli, input="y") + result = runner.invoke(cli, ["--update-environment"]) assert result.exit_code == 0 - orm.db_session.__enter__() - - create_database( - "sqlite", tmp_path.joinpath(".pytask.sqlite3").as_posix(), True, False - ) + with orm.db_session: + create_database( + "sqlite", tmp_path.joinpath(".pytask.sqlite3").as_posix(), True, False + ) + python = Environment["python"] - python = Environment["python"] assert python.version == fake_version assert python.path == "new_path" - - orm.db_session.__exit__() From 4bedaceaae762bbff439cb22c80e5fc4ea5c3cbc Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Tue, 25 Jan 2022 02:16:36 +0100 Subject: [PATCH 03/16] Update readme. --- .conda/meta.yaml | 46 ------------------------------- MANIFEST.in | 3 +- README.rst | 20 ++++++++++++-- _static/error.png | Bin 0 -> 46776 bytes src/pytask_environment/config.py | 5 ++-- 5 files changed, 21 insertions(+), 53 deletions(-) delete mode 100644 .conda/meta.yaml create mode 100644 _static/error.png diff --git a/.conda/meta.yaml b/.conda/meta.yaml deleted file mode 100644 index 1b6741a..0000000 --- a/.conda/meta.yaml +++ /dev/null @@ -1,46 +0,0 @@ -{% set data = load_setup_py_data() %} - -package: - name: pytask-environment - version: {{ data.get('version') }} - -source: - # git_url is nice in that it won't capture devenv stuff. However, it only captures - # committed code, so pay attention. - git_url: ../ - -build: - noarch: python - number: 0 - script: {{ PYTHON }} setup.py install --single-version-externally-managed --record record.txt - -requirements: - host: - - python - - pip - - setuptools - - run: - - python >=3.6 - - pytask >=0.0.7 - -test: - requires: - - pytest - source_files: - - tox.ini - - tests - commands: - - pytask --version - - pytask --help - - pytask clean - - pytask markers - - - pytest tests - -about: - home: https://github.com/pytask-dev/pytask-environment - license: MIT - license_file: LICENSE - summary: Ensure checks on the current Python environment. - dev_url: https://github.com/pytask-dev/pytask-environment/ diff --git a/MANIFEST.in b/MANIFEST.in index 4ac73fe..f450948 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,3 @@ -prune .conda prune tests exclude *.rst @@ -8,3 +7,5 @@ exclude tox.ini include README.rst include LICENSE + +recursive-include _static *.png diff --git a/README.rst b/README.rst index 218ed12..82df575 100644 --- a/README.rst +++ b/README.rst @@ -57,9 +57,23 @@ with Usage ----- -If the user attempts to build the project and the Python version has been cached in the -database in a previous run, an invocation with a different environment will produce the -following command line output. +If the user attempts to build the project with ``pytask build`` and the Python version +has been cached in the database in a previous run, an invocation with a different +environment will produce the following command line output. + +.. image:: _static/error.png + +Running the same command with ``pytask --update-environment`` will update the +information on the environment. + +To disable either checking the path or the version, set the following configuration to a +falsy value. + +.. code-block:: ini + + check_python_version = False # True by default + + check_environment = False # True by default Future development diff --git a/_static/error.png b/_static/error.png new file mode 100644 index 0000000000000000000000000000000000000000..74ebe1f33737c4858870f38a598cc2f8556acbda GIT binary patch literal 46776 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 Date: Tue, 25 Jan 2022 02:19:13 +0100 Subject: [PATCH 04/16] abort. --- .github/workflows/continuous-integration-workflow.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/continuous-integration-workflow.yml b/.github/workflows/continuous-integration-workflow.yml index 207e780..edca86c 100644 --- a/.github/workflows/continuous-integration-workflow.yml +++ b/.github/workflows/continuous-integration-workflow.yml @@ -7,6 +7,10 @@ on: branches: - '*' +concurrency: + group: ${{ github.head_ref || github.run_id }} + cancel-in-progress: true + jobs: run-tests: From 36f2c40ef987fe93b9858a3cc62514ad2143b089 Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Tue, 25 Jan 2022 02:28:25 +0100 Subject: [PATCH 05/16] Fix pre-commit. --- .pre-commit-config.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c7306ad..4f7ee98 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,7 +6,6 @@ repos: args: ['--maxkb=100'] - id: check-merge-conflict - id: check-yaml - exclude: meta.yaml - id: debug-statements - id: end-of-file-fixer - repo: https://github.com/pre-commit/pygrep-hooks From 2207133f2ef74d1cd400b8228e9f8950d6a546c5 Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Tue, 25 Jan 2022 02:28:48 +0100 Subject: [PATCH 06/16] Abort previous test. --- .github/workflows/continuous-integration-workflow.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/continuous-integration-workflow.yml b/.github/workflows/continuous-integration-workflow.yml index edca86c..f8c2ca0 100644 --- a/.github/workflows/continuous-integration-workflow.yml +++ b/.github/workflows/continuous-integration-workflow.yml @@ -8,7 +8,7 @@ on: - '*' concurrency: - group: ${{ github.head_ref || github.run_id }} + group: ${{ github.head_ref }} cancel-in-progress: true jobs: From 08b2e59898900595b8aa85356d4273b65169c654 Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Tue, 25 Jan 2022 02:33:36 +0100 Subject: [PATCH 07/16] execute without xdist. --- .github/workflows/continuous-integration-workflow.yml | 2 +- environment.yml | 1 - tox.ini | 3 +-- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/continuous-integration-workflow.yml b/.github/workflows/continuous-integration-workflow.yml index f8c2ca0..d5a52e3 100644 --- a/.github/workflows/continuous-integration-workflow.yml +++ b/.github/workflows/continuous-integration-workflow.yml @@ -38,7 +38,7 @@ jobs: - name: Run end-to-end tests. shell: bash -l {0} - run: tox -e pytest -- -m end_to_end --cov=./ --cov-report=xml -n auto + run: tox -e pytest -- -m end_to_end --cov=./ --cov-report=xml - name: Upload coverage reports of end-to-end tests. if: runner.os == 'Linux' && matrix.python-version == '3.8' diff --git a/environment.yml b/environment.yml index e6a972e..6d13e82 100644 --- a/environment.yml +++ b/environment.yml @@ -24,4 +24,3 @@ dependencies: - pdbpp - pre-commit - pytest-cov - - pytest-xdist diff --git a/tox.ini b/tox.ini index dca27f8..15c3f82 100644 --- a/tox.ini +++ b/tox.ini @@ -12,7 +12,6 @@ conda_deps = pytask >=0.0.7 pytest pytest-cov - pytest-xdist conda_channels = conda-forge nodefaults @@ -22,7 +21,7 @@ commands = [doc8] ignore = D002, D004 -max-line-length = 89 +max-line-length = 88 [flake8] docstring-convention = numpy From 16a38848d21013d0029de6fc5d98f6a2efcd38d9 Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Tue, 25 Jan 2022 02:34:56 +0100 Subject: [PATCH 08/16] Print output. --- tests/test_collect.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_collect.py b/tests/test_collect.py index eee9f73..d7f7f49 100644 --- a/tests/test_collect.py +++ b/tests/test_collect.py @@ -62,16 +62,19 @@ def test_prompt_when_python_version_has_changed(monkeypatch, tmp_path, runner): # Run without knowing the python version and without updating the environment. result = runner.invoke(cli) + print(result.output) assert result.exit_code == 1 # Run with updating the environment. result = runner.invoke(cli, ["--update-environment"]) + print(result.output) assert result.exit_code == 0 # Run with a fake version and not updating the environment. monkeypatch.setattr("pytask_environment.collect.sys.version", fake_version) result = runner.invoke(cli) + print(result.output) assert result.exit_code == 1 with orm.db_session: @@ -88,6 +91,7 @@ def test_prompt_when_python_version_has_changed(monkeypatch, tmp_path, runner): monkeypatch.setattr("pytask_environment.collect.sys.executable", "new_path") result = runner.invoke(cli, ["--update-environment"]) + print(result.output) assert result.exit_code == 0 with orm.db_session: From 46dfa73dd0c8d600cfec77c1e41f8f3bb88c4dae Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Tue, 25 Jan 2022 02:40:06 +0100 Subject: [PATCH 09/16] remove print: --- tests/test_collect.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/test_collect.py b/tests/test_collect.py index d7f7f49..eee9f73 100644 --- a/tests/test_collect.py +++ b/tests/test_collect.py @@ -62,19 +62,16 @@ def test_prompt_when_python_version_has_changed(monkeypatch, tmp_path, runner): # Run without knowing the python version and without updating the environment. result = runner.invoke(cli) - print(result.output) assert result.exit_code == 1 # Run with updating the environment. result = runner.invoke(cli, ["--update-environment"]) - print(result.output) assert result.exit_code == 0 # Run with a fake version and not updating the environment. monkeypatch.setattr("pytask_environment.collect.sys.version", fake_version) result = runner.invoke(cli) - print(result.output) assert result.exit_code == 1 with orm.db_session: @@ -91,7 +88,6 @@ def test_prompt_when_python_version_has_changed(monkeypatch, tmp_path, runner): monkeypatch.setattr("pytask_environment.collect.sys.executable", "new_path") result = runner.invoke(cli, ["--update-environment"]) - print(result.output) assert result.exit_code == 0 with orm.db_session: From ae14f65f4b10dbf9224f3c13a8d4c341d2c745ad Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Tue, 25 Jan 2022 02:41:07 +0100 Subject: [PATCH 10/16] extend changes. --- CHANGES.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index c259e20..e7bd584 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -10,7 +10,8 @@ reverse chronological order. Releases follow `semantic versioning 0.1.0 - 2022-01-25 ------------------ -- :gh:`10` replaces the input prompts with configuration values and flags. +- :gh:`10` replaces the input prompts with configuration values and flags, removes the + conda recipe, and abort simultaneously running builds. 0.0.6 - 2021-07-23 From 41c0d49ae5ed111c934a3b5c6fbc439a2d30a0aa Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Tue, 25 Jan 2022 02:44:15 +0100 Subject: [PATCH 11/16] extend readme. --- README.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.rst b/README.rst index 82df575..2f61320 100644 --- a/README.rst +++ b/README.rst @@ -71,6 +71,8 @@ falsy value. .. code-block:: ini + # Content of pytask.ini, setup.cfg, or tox.ini + check_python_version = False # True by default check_environment = False # True by default From e89fa7b7eb26a485485b3ac4aaf67eb063047834 Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Tue, 25 Jan 2022 09:20:47 +0100 Subject: [PATCH 12/16] some fixes. --- README.rst | 4 ++-- src/pytask_environment/collect.py | 21 ++++++++++++++++----- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/README.rst b/README.rst index 2f61320..c1e4934 100644 --- a/README.rst +++ b/README.rst @@ -73,9 +73,9 @@ falsy value. # Content of pytask.ini, setup.cfg, or tox.ini - check_python_version = False # True by default + check_python_version = false # true by default - check_environment = False # True by default + check_environment = false # true by default Future development diff --git a/src/pytask_environment/collect.py b/src/pytask_environment/collect.py index 9da9028..5d0e1b0 100644 --- a/src/pytask_environment/collect.py +++ b/src/pytask_environment/collect.py @@ -26,14 +26,25 @@ def pytask_log_session_header(session) -> None: """ __tracebackhide__ = True + # If no checks are requested, skip. + if ( + not session.config["check_python_version"] + and not session.config["check_environment"] + ): + return None + package = retrieve_package("python") - same_version = True if package is None else sys.version == package.version - same_path = True if package is None else sys.executable == package.path + same_version = False if package is None else sys.version == package.version + same_path = False if package is None else sys.executable == package.path + + # Bail out if everything is fine. + if same_version and same_path: + return None msg = "" if not same_version and session.config["check_python_version"]: - msg += " The Python version has changed " + msg += "The Python version has changed " if package is not None: msg += f"from\n\n{package.version}\n\n" msg += f"to\n\n{sys.version}\n\n" @@ -44,10 +55,10 @@ def pytask_log_session_header(session) -> None: msg += f"to\n\n{sys.executable}." if msg: - msg = "Your Python environment has changed." + msg + msg = "Your Python environment has changed. " + msg if session.config["update_environment"] or package is None: - console.print("Update the information in the database.") + console.print("Updating the information in the database.") create_or_update_state("python", sys.version, sys.executable) else: console.print() From e1cb12da7a82ecbd6c6baf2ab3ce998aba4a30e5 Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Wed, 26 Jan 2022 08:31:39 +0100 Subject: [PATCH 13/16] Add more tests. --- src/pytask_environment/collect.py | 10 ++- tests/test_collect.py | 108 ++++++++++++++++++++++-------- 2 files changed, 88 insertions(+), 30 deletions(-) diff --git a/src/pytask_environment/collect.py b/src/pytask_environment/collect.py index 5d0e1b0..8bcd8d2 100644 --- a/src/pytask_environment/collect.py +++ b/src/pytask_environment/collect.py @@ -7,9 +7,9 @@ _ERROR_MSG = """\ -Aborted execution due to a bad state of the environment. Either switch to the correct -environment or update the information on the environment using the --update-environment -flag. +Aborted execution due to a bad state of the environment. Either switch to the correct \ +environment or update the information on the environment using the --update-environment\ + flag. """ @@ -60,6 +60,10 @@ def pytask_log_session_header(session) -> None: if session.config["update_environment"] or package is None: console.print("Updating the information in the database.") create_or_update_state("python", sys.version, sys.executable) + elif (not same_version and not session.config["check_python_version"]) and ( + not same_path and not session.config["check_python_version"] + ): + pass else: console.print() raise Exception(msg + "\n\n" + _ERROR_MSG) from None diff --git a/tests/test_collect.py b/tests/test_collect.py index eee9f73..a909357 100644 --- a/tests/test_collect.py +++ b/tests/test_collect.py @@ -1,9 +1,8 @@ -import os import sys import textwrap import pytest -from _pytask.database import create_database +from _pytask.database import db from pony import orm from pytask import cli from pytask_environment.database import Environment @@ -14,22 +13,21 @@ def test_existence_of_python_executable_in_db(tmp_path, runner): """Test that the Python executable is stored in the database.""" task_path = tmp_path.joinpath("task_dummy.py") task_path.write_text(textwrap.dedent("def task_dummy(): pass")) + tmp_path.joinpath("pytask.ini").write_text("[pytask]") - os.chdir(tmp_path) - result = runner.invoke(cli) + result = runner.invoke(cli, [tmp_path.as_posix()]) assert result.exit_code == 0 with orm.db_session: - - create_database( - "sqlite", tmp_path.joinpath(".pytask.sqlite3").as_posix(), True, False - ) - python = Environment["python"] - assert python.version == sys.version - assert python.path == sys.executable + assert python.version == sys.version + assert python.path == sys.executable + + orm.rollback() + for entity in db.entities.values(): + orm.delete(e for e in entity) @pytest.mark.skipif( @@ -37,7 +35,7 @@ def test_existence_of_python_executable_in_db(tmp_path, runner): reason="Error on Windows with Python 3.6", ) @pytest.mark.end_to_end -def test_prompt_when_python_version_has_changed(monkeypatch, tmp_path, runner): +def test_flow_when_python_version_has_changed(monkeypatch, tmp_path, runner): """Test the whole use-case. 1. Run a simple task to cache the Python version and path. @@ -54,19 +52,15 @@ def test_prompt_when_python_version_has_changed(monkeypatch, tmp_path, runner): "[MSC v.1916 64 bit (AMD64)]" ) + tmp_path.joinpath("pytask.ini").write_text("[pytask]") source = "def task_dummy(): pass" task_path = tmp_path.joinpath("task_dummy.py") task_path.write_text(textwrap.dedent(source)) - os.chdir(tmp_path) - # Run without knowing the python version and without updating the environment. - result = runner.invoke(cli) - assert result.exit_code == 1 - - # Run with updating the environment. - result = runner.invoke(cli, ["--update-environment"]) + result = runner.invoke(cli, [tmp_path.as_posix()]) assert result.exit_code == 0 + assert "Updating the information" in result.output # Run with a fake version and not updating the environment. monkeypatch.setattr("pytask_environment.collect.sys.version", fake_version) @@ -75,9 +69,6 @@ def test_prompt_when_python_version_has_changed(monkeypatch, tmp_path, runner): assert result.exit_code == 1 with orm.db_session: - create_database( - "sqlite", tmp_path.joinpath(".pytask.sqlite3").as_posix(), True, False - ) python = Environment["python"] assert python.version == real_python_version @@ -91,10 +82,73 @@ def test_prompt_when_python_version_has_changed(monkeypatch, tmp_path, runner): assert result.exit_code == 0 with orm.db_session: - create_database( - "sqlite", tmp_path.joinpath(".pytask.sqlite3").as_posix(), True, False - ) python = Environment["python"] - assert python.version == fake_version - assert python.path == "new_path" + assert python.version == fake_version + assert python.path == "new_path" + + orm.rollback() + for entity in db.entities.values(): + orm.delete(e for e in entity) + + +@pytest.mark.end_to_end +@pytest.mark.parametrize("check_python_version, expected", [("true", 1), ("false", 0)]) +def test_python_version_changed( + monkeypatch, tmp_path, runner, check_python_version, expected +): + fake_version = ( + "2.7.8 | packaged by conda-forge | (default, Jul 31 2020, 01:53:57) " + "[MSC v.1916 64 bit (AMD64)]" + ) + tmp_path.joinpath("pytask.ini").write_text( + f"[pytask]\ncheck_python_version = {check_python_version}" + ) + source = "def task_dummy(): pass" + task_path = tmp_path.joinpath("task_dummy.py") + task_path.write_text(textwrap.dedent(source)) + + # Run without knowing the python version and without updating the environment. + result = runner.invoke(cli, [tmp_path.as_posix()]) + assert result.exit_code == 0 + assert "Updating the information" in result.output + + # Run with a fake version and not updating the environment. + monkeypatch.setattr("pytask_environment.collect.sys.version", fake_version) + + result = runner.invoke(cli, [tmp_path.as_posix()]) + assert result.exit_code == expected + + with orm.db_session: + orm.rollback() + for entity in db.entities.values(): + orm.delete(e for e in entity) + + +@pytest.mark.end_to_end +@pytest.mark.parametrize("check_python_version, expected", [("true", 1), ("false", 0)]) +def test_environment_changed( + monkeypatch, tmp_path, runner, check_python_version, expected +): + tmp_path.joinpath("pytask.ini").write_text( + f"[pytask]\ncheck_environment = {check_python_version}" + ) + source = "def task_dummy(): pass" + task_path = tmp_path.joinpath("task_dummy.py") + task_path.write_text(textwrap.dedent(source)) + + # Run without knowing the python version and without updating the environment. + result = runner.invoke(cli, [tmp_path.as_posix()]) + assert result.exit_code == 0 + assert "Updating the information" in result.output + + # Run with a fake version and not updating the environment. + monkeypatch.setattr("pytask_environment.collect.sys.executable", "new_path") + + result = runner.invoke(cli, [tmp_path.as_posix()]) + assert result.exit_code == expected + + with orm.db_session: + orm.rollback() + for entity in db.entities.values(): + orm.delete(e for e in entity) From 86fa1e8a57d591d3fb1ea63c811cf315d3d2d107 Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Wed, 26 Jan 2022 08:33:15 +0100 Subject: [PATCH 14/16] Rename to logging. --- src/pytask_environment/{collect.py => logging.py} | 0 src/pytask_environment/plugin.py | 4 ++-- tests/{test_collect.py => test_logging.py} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename src/pytask_environment/{collect.py => logging.py} (100%) rename tests/{test_collect.py => test_logging.py} (100%) diff --git a/src/pytask_environment/collect.py b/src/pytask_environment/logging.py similarity index 100% rename from src/pytask_environment/collect.py rename to src/pytask_environment/logging.py diff --git a/src/pytask_environment/plugin.py b/src/pytask_environment/plugin.py index ae3441b..9fa319e 100644 --- a/src/pytask_environment/plugin.py +++ b/src/pytask_environment/plugin.py @@ -1,13 +1,13 @@ """Entry-point for the plugin.""" from _pytask.config import hookimpl -from pytask_environment import collect from pytask_environment import config from pytask_environment import database +from pytask_environment import logging @hookimpl def pytask_add_hooks(pm): """Register some plugins.""" - pm.register(collect) + pm.register(logging) pm.register(config) pm.register(database) diff --git a/tests/test_collect.py b/tests/test_logging.py similarity index 100% rename from tests/test_collect.py rename to tests/test_logging.py From 6163abee92695a81c8e7dcd7be961fac89988705 Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Wed, 26 Jan 2022 08:34:29 +0100 Subject: [PATCH 15/16] more beautiful docs. --- README.rst | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index c1e4934..6a599ce 100644 --- a/README.rst +++ b/README.rst @@ -63,8 +63,13 @@ environment will produce the following command line output. .. image:: _static/error.png -Running the same command with ``pytask --update-environment`` will update the -information on the environment. +Running + +.. code-block:: console + + $ pytask --update-environment + +will update the information on the environment. To disable either checking the path or the version, set the following configuration to a falsy value. From 68d8c5a57f529717715fe12d1a59a6f088b4e00d Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Wed, 26 Jan 2022 08:39:53 +0100 Subject: [PATCH 16/16] Fix. --- src/pytask_environment/logging.py | 4 +--- tests/test_logging.py | 10 +++++----- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/pytask_environment/logging.py b/src/pytask_environment/logging.py index 8bcd8d2..98dbc44 100644 --- a/src/pytask_environment/logging.py +++ b/src/pytask_environment/logging.py @@ -60,9 +60,7 @@ def pytask_log_session_header(session) -> None: if session.config["update_environment"] or package is None: console.print("Updating the information in the database.") create_or_update_state("python", sys.version, sys.executable) - elif (not same_version and not session.config["check_python_version"]) and ( - not same_path and not session.config["check_python_version"] - ): + elif not msg: pass else: console.print() diff --git a/tests/test_logging.py b/tests/test_logging.py index a909357..b160884 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -63,7 +63,7 @@ def test_flow_when_python_version_has_changed(monkeypatch, tmp_path, runner): assert "Updating the information" in result.output # Run with a fake version and not updating the environment. - monkeypatch.setattr("pytask_environment.collect.sys.version", fake_version) + monkeypatch.setattr("pytask_environment.logging.sys.version", fake_version) result = runner.invoke(cli) assert result.exit_code == 1 @@ -75,8 +75,8 @@ def test_flow_when_python_version_has_changed(monkeypatch, tmp_path, runner): assert python.path == real_python_executable # Run with a fake version and updating the environment. - monkeypatch.setattr("pytask_environment.collect.sys.version", fake_version) - monkeypatch.setattr("pytask_environment.collect.sys.executable", "new_path") + monkeypatch.setattr("pytask_environment.logging.sys.version", fake_version) + monkeypatch.setattr("pytask_environment.logging.sys.executable", "new_path") result = runner.invoke(cli, ["--update-environment"]) assert result.exit_code == 0 @@ -114,7 +114,7 @@ def test_python_version_changed( assert "Updating the information" in result.output # Run with a fake version and not updating the environment. - monkeypatch.setattr("pytask_environment.collect.sys.version", fake_version) + monkeypatch.setattr("pytask_environment.logging.sys.version", fake_version) result = runner.invoke(cli, [tmp_path.as_posix()]) assert result.exit_code == expected @@ -143,7 +143,7 @@ def test_environment_changed( assert "Updating the information" in result.output # Run with a fake version and not updating the environment. - monkeypatch.setattr("pytask_environment.collect.sys.executable", "new_path") + monkeypatch.setattr("pytask_environment.logging.sys.executable", "new_path") result = runner.invoke(cli, [tmp_path.as_posix()]) assert result.exit_code == expected