diff --git a/.flake8 b/.flake8 deleted file mode 100644 index fba48b089d..0000000000 --- a/.flake8 +++ /dev/null @@ -1,8 +0,0 @@ -# Black-compatible flake8 config - -[flake8] -ignore = E203, E266, E402, E501, W503, C901 -max-line-length = 80 -max-complexity = 18 -select = B,C,E,F,W,T4,B9 -exclude = docs diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 71833dd993..79bdbf8761 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,15 +1,16 @@ default_language_version: python: python3 repos: - - repo: https://github.com/ambv/black - rev: 22.3.0 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.15.8 hooks: - - id: black + - id: ruff + args: [--fix] + - id: ruff-format - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.0.0 + rev: v5.0.0 hooks: - id: check-merge-conflict - - id: flake8 - id: debug-statements exclude: "cumulusci/(utils/logging|cli/cci|tasks/robotframework/debugger/ui|cli/task|robotframework/utils).py" - repo: https://github.com/Lucas-C/pre-commit-hooks-markup @@ -17,11 +18,6 @@ repos: hooks: - id: rst-linter exclude: "docs" - - repo: https://github.com/pycqa/isort - rev: 5.12.0 - hooks: - - id: isort - args: ["--profile", "black", "--filter-files"] - repo: https://github.com/pre-commit/mirrors-prettier rev: v3.1.0 hooks: diff --git a/.prettierignore b/.prettierignore index 06c37e2dbb..0c3bdba138 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,5 +1,13 @@ -Test*.yaml +# Jinja-templated HTML: Prettier lowercases , which breaks +# xml.etree.ElementTree.parse() in RobotLibDoc tests. +cumulusci/tasks/robotframework/template.html + +# VCR cassettes are YAML-shaped but not reliably parseable by Prettier's YAML +# formatter (multi-line quoted bodies, anchors, etc.). +**/cassettes/** + +# Bundled third-party diagram assets (minified JS/CSS). docs/diagram/ -**/*.min.js -**/*.min.css -cumulusci/files/templates/ \ No newline at end of file + +# Jinja-templated JSON/YAML shipped as project templates. +cumulusci/files/templates/ diff --git a/.readthedocs.yml b/.readthedocs.yml index 5be9040284..5091f06ea8 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -27,7 +27,7 @@ sphinx: formats: - pdf - epub - # Optionally declare the Python requirements required to build your docs + # Optionally declare the Python requirements required to build your docs # python: # install: # - requirements: requirements_dev.txt diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index c15c10694e..2cc9a8d22e 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -35,23 +35,23 @@ socioeconomic status, or other similar personal characteristics. Examples of behavior that contributes to creating a positive environment include: -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy toward other community members +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy toward other community members Examples of unacceptable behavior by participants include: -* The use of sexualized language or imagery and unwelcome sexual attention or -advances -* Personal attacks, insulting/derogatory comments, or trolling -* Public or private harassment -* Publishing, or threatening to publish, others' private information—such as -a physical or electronic address—without explicit permission -* Other conduct which could reasonably be considered inappropriate in a -professional setting -* Advocating for or encouraging any of the above behaviors +- The use of sexualized language or imagery and unwelcome sexual attention or + advances +- Personal attacks, insulting/derogatory comments, or trolling +- Public or private harassment +- Publishing, or threatening to publish, others' private information—such as + a physical or electronic address—without explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting +- Advocating for or encouraging any of the above behaviors ## Our Responsibilities @@ -98,7 +98,7 @@ It includes adaptions and additions from [Go Community Code of Conduct][golang-c This Code of Conduct is licensed under the [Creative Commons Attribution 3.0 License][cc-by-3-us]. -[contributor-covenant-home]: https://www.contributor-covenant.org (https://www.contributor-covenant.org/) +[contributor-covenant-home]: https://www.contributor-covenant.org "https://www.contributor-covenant.org/" [golang-coc]: https://golang.org/conduct [cncf-coc]: https://github.com/cncf/foundation/blob/master/code-of-conduct.md [microsoft-coc]: https://opensource.microsoft.com/codeofconduct/ diff --git a/Makefile b/Makefile index 19b7ada463..b54cec51fc 100644 --- a/Makefile +++ b/Makefile @@ -40,20 +40,20 @@ clean-pyc: ## remove Python file artifacts find . -name '__pycache__' -exec rm -fr {} + clean-test: ## remove test and coverage artifacts - rm -fr .tox/ rm -f .coverage rm -fr htmlcov/ rm -f output.xml rm -f report.html -lint: ## check style with flake8 - flake8 cumulusci tests +lint: ## check style with ruff and pyright + ruff check cumulusci tests + ruff format --check cumulusci tests test: ## run tests quickly with the default Python pytest -test-all: ## run tests on every Python version with tox - tox +test-all: ## run tests on every Python version via CI matrix + @echo "Multi-version testing runs in CI. Use 'pytest' for local testing." # Use CLASS_PATH to run coverage for a subset of tests. # $ make coverage CLASS_PATH="cumulusci/core/tests" @@ -82,7 +82,6 @@ servedocs: docs ## compile the docs watching for changes watchmedo shell-command -p '*.rst' -c '$(MAKE) -C docs html' -R -D . release: clean ## package and upload a release - python utility/pin_dependencies.py hatch build hatch publish @@ -97,15 +96,11 @@ tag: clean git tag -a -m 'version $$(hatch version)' v$$(hatch version) git push --follow-tags -update-deps: - echo Use the _Update Python Dependencies_ Github action for real releases - pip-compile --upgrade --resolver=backtracking --output-file=requirements/prod.txt pyproject.toml - pip-compile --upgrade --resolver=backtracking --output-file=requirements/dev.txt --all-extras pyproject.toml +update-deps: ## update all dependencies via uv + uv lock --upgrade -dev-install: - python -m pip install --upgrade pip pip-tools setuptools - pip-sync requirements/*.txt - python -m pip install -e . +dev-install: ## install development dependencies via uv + uv sync --group dev schema: python -c 'from cumulusci.utils.yaml import cumulusci_yml; open("cumulusci/schema/cumulusci.jsonschema.json", "w").write(cumulusci_yml.CumulusCIRoot.schema_json(indent=4))' diff --git a/SECURITY.md b/SECURITY.md index e31774df28..8249025739 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,4 +4,4 @@ Please report any security issue to [security@salesforce.com](mailto:security@sa as soon as it is discovered. This library limits its runtime dependencies in order to reduce the total cost of ownership as much as can be, but all consumers should remain vigilant and have their security stakeholders review all third-party -products (3PP) like this one and their dependencies. \ No newline at end of file +products (3PP) like this one and their dependencies. diff --git a/cumulusci/__init__.py b/cumulusci/__init__.py index 2db01717a4..01c39a428e 100644 --- a/cumulusci/__init__.py +++ b/cumulusci/__init__.py @@ -14,5 +14,5 @@ if sys.version_info < (3, 8): # pragma: no cover raise Exception("CumulusCI requires Python 3.8+.") -api.OrderedDict = dict -bulk.OrderedDict = dict +api.OrderedDict = dict # pyright: ignore[reportPrivateImportUsage] +bulk.OrderedDict = dict # pyright: ignore[reportPrivateImportUsage] diff --git a/cumulusci/cli/flow.py b/cumulusci/cli/flow.py index 96bd8db9cf..3e3c2768e5 100644 --- a/cumulusci/cli/flow.py +++ b/cumulusci/cli/flow.py @@ -44,9 +44,9 @@ def flow_doc(runtime, project=False): flows_by_group = group_items(flows) flow_groups = sorted( flows_by_group.keys(), - key=lambda group: flow_info_groups.index(group) - if group in flow_info_groups - else 100, + key=lambda group: ( + flow_info_groups.index(group) if group in flow_info_groups else 100 + ), ) for group in flow_groups: diff --git a/cumulusci/cli/logger.py b/cumulusci/cli/logger.py index 0e5461f829..e045ab43d6 100644 --- a/cumulusci/cli/logger.py +++ b/cumulusci/cli/logger.py @@ -1,4 +1,5 @@ -""" CLI logger """ +"""CLI logger""" + import logging import os import sys diff --git a/cumulusci/cli/org.py b/cumulusci/cli/org.py index 3d2d08fc7a..6951206429 100644 --- a/cumulusci/cli/org.py +++ b/cumulusci/cli/org.py @@ -34,6 +34,7 @@ def set_org_name(required): `required` is a boolean for whether org_name is required """ + # could be generalized to work for any mutex pair (or list) but no obvious need def callback(ctx, param, value): """Callback which enforces mutex and 'required' behaviour (if required).""" @@ -474,7 +475,6 @@ def org_prune(runtime, include_active=False): org_shapes_skipped = [] active_orgs_skipped = [] for org_name in runtime.keychain.list_orgs(): - org_config = runtime.keychain.get_org(org_name) if org_name in predefined_scratch_configs: diff --git a/cumulusci/cli/project.py b/cumulusci/cli/project.py index 9111259b83..fc77fa693a 100644 --- a/cumulusci/cli/project.py +++ b/cumulusci/cli/project.py @@ -246,7 +246,6 @@ def init_from_context(context: Dict[str, object], echo: bool = False): # Create sfdx-project.json if not os.path.isfile("sfdx-project.json"): - sfdx_project = { "packageDirectories": [{"path": "force-app", "default": True}], "namespace": context["package_namespace"], diff --git a/cumulusci/cli/runtime.py b/cumulusci/cli/runtime.py index 9e596fa7da..8635bc7f36 100644 --- a/cumulusci/cli/runtime.py +++ b/cumulusci/cli/runtime.py @@ -23,7 +23,7 @@ def __init__(self, *args, **kwargs): super(CliRuntime, self).__init__(*args, **kwargs) except ConfigError as e: raise click.UsageError(f"Config Error: {str(e)}") - except (KeychainKeyNotFound) as e: + except KeychainKeyNotFound as e: raise click.UsageError(f"Keychain Error: {str(e)}") def get_keychain_class(self): diff --git a/cumulusci/cli/service.py b/cumulusci/cli/service.py index 6beaaf9fbf..d6590e3158 100644 --- a/cumulusci/cli/service.py +++ b/cumulusci/cli/service.py @@ -69,7 +69,7 @@ def service_list(runtime, plain, print_json): console.print(table) -class ConnectServiceCommand(click.MultiCommand): +class ConnectServiceCommand(click.Group): def _get_services_config(self, runtime): return ( runtime.project_config.services diff --git a/cumulusci/cli/task.py b/cumulusci/cli/task.py index cfbe749b91..344f973a96 100644 --- a/cumulusci/cli/task.py +++ b/cumulusci/cli/task.py @@ -107,7 +107,7 @@ def task_info(runtime, task_name): click.echo(rst2ansi(doc)) -class RunTaskCommand(click.MultiCommand): +class RunTaskCommand(click.Group): # options that are not task specific global_options = { "no-prompt": { diff --git a/cumulusci/cli/tests/test_error.py b/cumulusci/cli/tests/test_error.py index 094a767896..ccfb6c10bb 100644 --- a/cumulusci/cli/tests/test_error.py +++ b/cumulusci/cli/tests/test_error.py @@ -98,7 +98,9 @@ def test_error_gist( ) webbrowser_open.assert_called_once_with(expected_gist_url) - @pytest.mark.skipif(sys.version_info > (3, 11), reason="requires python3.10 or higher") + @pytest.mark.skipif( + sys.version_info > (3, 11), reason="requires python3.10 or higher" + ) @mock.patch("cumulusci.cli.error.platform") @mock.patch("cumulusci.cli.error.sys") @mock.patch("cumulusci.cli.error.datetime") diff --git a/cumulusci/cli/tests/test_org.py b/cumulusci/cli/tests/test_org.py index c85f2e0507..494dec3b4b 100644 --- a/cumulusci/cli/tests/test_org.py +++ b/cumulusci/cli/tests/test_org.py @@ -887,9 +887,9 @@ def get_org(orgname): run_click_command(org.org_list, runtime=runtime, json_flag=False, plain=False) - assert "Cannot load org config for `test1`" in str( + assert "Cannot load org config for `test1`" in str(echo.mock_calls), ( echo.mock_calls - ), echo.mock_calls + ) assert "NOPE!" in str(echo.mock_calls), echo.mock_calls assert "Cannot cleanup org cache dirs" in str(echo.mock_calls), echo.mock_calls diff --git a/cumulusci/cli/tests/test_plan.py b/cumulusci/cli/tests/test_plan.py index 1dbc55da38..d73ab432ed 100644 --- a/cumulusci/cli/tests/test_plan.py +++ b/cumulusci/cli/tests/test_plan.py @@ -148,16 +148,18 @@ def test_plan_info__config(self, cli_table, runtime): run_click_command( plan.plan_info, "plan 1", runtime=runtime, messages_only=False ) - cli_table.assert_any_call( - title="Config", - data=[ - ["Key", "Value"], - ["YAML Key", "plan 1"], - ["Slug", "plan1_slug"], - ["Tier", "primary"], - ["Hidden?", False], - ], - ), + ( + cli_table.assert_any_call( + title="Config", + data=[ + ["Key", "Value"], + ["YAML Key", "plan 1"], + ["Slug", "plan1_slug"], + ["Tier", "primary"], + ["Hidden?", False], + ], + ), + ) @mock.patch("cumulusci.cli.plan.CliTable") def test_plan_info__messages(self, cli_table, runtime): @@ -182,17 +184,19 @@ def test_plan_info__preflight_checks(self, cli_table, runtime): run_click_command( plan.plan_info, "plan 1", runtime=runtime, messages_only=False ) - cli_table.assert_any_call( - title="Plan Preflights", - data=[ - ["Action", "Message", "When"], - [ - "error", - "Test Package must be installed in your org.", - "'test package' not in tasks.get_installed_packages()", + ( + cli_table.assert_any_call( + title="Plan Preflights", + data=[ + ["Action", "Message", "When"], + [ + "error", + "Test Package must be installed in your org.", + "'test package' not in tasks.get_installed_packages()", + ], ], - ], - ), + ), + ) @mock.patch("cumulusci.cli.plan.CliTable") def test_plan_info__step_preflight_checks(self, cli_table, runtime): @@ -200,13 +204,15 @@ def test_plan_info__step_preflight_checks(self, cli_table, runtime): run_click_command( plan.plan_info, "plan 1", runtime=runtime, messages_only=False ) - cli_table.assert_any_call( - title="Step Preflights", - data=[ - ["Step", "Action", "Message", "When"], - [1, "error", "Danger Will Robinson!", "soon"], - ], - ), + ( + cli_table.assert_any_call( + title="Step Preflights", + data=[ + ["Step", "Action", "Message", "When"], + [1, "error", "Danger Will Robinson!", "soon"], + ], + ), + ) @mock.patch("cumulusci.cli.plan.CliTable") def test_plan_info__steps(self, cli_table, runtime): diff --git a/cumulusci/cli/tests/test_project.py b/cumulusci/cli/tests/test_project.py index d296cdf838..511a02254f 100644 --- a/cumulusci/cli/tests/test_project.py +++ b/cumulusci/cli/tests/test_project.py @@ -230,6 +230,4 @@ def test_render_recursive(self): - list \x1b[1mdict:\x1b[0m \x1b[1mkey:\x1b[0m value - \x1b[1mstr:\x1b[0m str""" == "\n".join( - out - ) + \x1b[1mstr:\x1b[0m str""" == "\n".join(out) diff --git a/cumulusci/cli/ui.py b/cumulusci/cli/ui.py index a809d8de42..651946c2a7 100644 --- a/cumulusci/cli/ui.py +++ b/cumulusci/cli/ui.py @@ -4,6 +4,7 @@ Classes: CliTable: Pretty prints tabular data to stdout, via Rich's Console API """ + import os from typing import Any, List, Union diff --git a/cumulusci/core/config/base_config.py b/cumulusci/core/config/base_config.py index 130ef3ac96..7ebd788dbc 100644 --- a/cumulusci/core/config/base_config.py +++ b/cumulusci/core/config/base_config.py @@ -28,9 +28,9 @@ def __init__(self, config: Optional[dict] = None, keychain=None): if not type_for_value: warnings.warn(f"{k}: {v} not declared for {type(self)}") if (v is not None) and (type_for_value is not None): - assert isinstance( - v, type_for_value - ), f"{k}: {v} should be of type {type_for_value}, not {type(v)} for {type(self)}" + assert isinstance(v, type_for_value), ( + f"{k}: {v} should be of type {type_for_value}, not {type(v)} for {type(self)}" + ) self.config = config.copy() self._init_logger() diff --git a/cumulusci/core/config/marketing_cloud_service_config.py b/cumulusci/core/config/marketing_cloud_service_config.py index 61fc376ad9..14015575ee 100644 --- a/cumulusci/core/config/marketing_cloud_service_config.py +++ b/cumulusci/core/config/marketing_cloud_service_config.py @@ -10,7 +10,6 @@ class MarketingCloudServiceConfig(OAuth2ServiceConfig): - refresh_token: str oauth2_client: str soap_instance_url: str diff --git a/cumulusci/core/config/project_config.py b/cumulusci/core/config/project_config.py index 72c3850e49..780ed05532 100644 --- a/cumulusci/core/config/project_config.py +++ b/cumulusci/core/config/project_config.py @@ -141,9 +141,7 @@ def config_project_local_path(self) -> Optional[str]: def _load_config(self): """Loads the configuration from YAML, if no override config was passed in initially.""" - if ( - self.config - ): # any config being pre-set at init will short circuit out, but not a plain {} + if self.config: # any config being pre-set at init will short circuit out, but not a plain {} return # Verify that we're in a project diff --git a/cumulusci/core/config/tests/_test_config_backwards_compatibility.py b/cumulusci/core/config/tests/_test_config_backwards_compatibility.py index 339fc545a6..36f8c477aa 100644 --- a/cumulusci/core/config/tests/_test_config_backwards_compatibility.py +++ b/cumulusci/core/config/tests/_test_config_backwards_compatibility.py @@ -12,7 +12,6 @@ class TestConfigBackwardsCompatibility: @patch.dict(os.environ) def test_temporary_backwards_compatibility_hacks(self): with pytest.warns(ClassMovedWarning): - from cumulusci.core.config.OrgConfig import OrgConfig assert isinstance(OrgConfig, type) diff --git a/cumulusci/core/config/tests/test_config.py b/cumulusci/core/config/tests/test_config.py index 77973526dd..11ad6350b2 100644 --- a/cumulusci/core/config/tests/test_config.py +++ b/cumulusci/core/config/tests/test_config.py @@ -60,13 +60,16 @@ def test_getattr_toplevel_key(self): def test_getattr_toplevel_key_missing(self): config = BaseConfig() config.config = {} - with mock.patch( - "cumulusci.core.config.base_config.STRICT_GETATTR", False - ), pytest.warns(DeprecationWarning, match="foo"): + with ( + mock.patch("cumulusci.core.config.base_config.STRICT_GETATTR", False), + pytest.warns(DeprecationWarning, match="foo"), + ): assert config.foo is None - with mock.patch( - "cumulusci.core.config.base_config.STRICT_GETATTR", True - ), pytest.deprecated_call(), pytest.raises(AssertionError): + with ( + mock.patch("cumulusci.core.config.base_config.STRICT_GETATTR", True), + pytest.deprecated_call(), + pytest.raises(AssertionError), + ): assert config.foo is None def test_getattr_child_key(self): @@ -77,9 +80,11 @@ def test_getattr_child_key(self): def test_strict_getattr(self): config = FakeConfig() config.config = {"foo": {"bar": "baz"}} - with mock.patch( - "cumulusci.core.config.base_config.STRICT_GETATTR", "True" - ), mock.patch("warnings.warn"), pytest.raises(AssertionError): + with ( + mock.patch("cumulusci.core.config.base_config.STRICT_GETATTR", "True"), + mock.patch("warnings.warn"), + pytest.raises(AssertionError), + ): print(config.jfiesojfieoj) def test_getattr_child_parent_key_missing(self): @@ -398,7 +403,7 @@ def test_repo_url_from_git(self, git_path): git_path.return_value = git_config_file repo_url = "https://github.com/foo/bar.git" with open(git_config_file, "w") as f: - f.writelines(['[remote "origin"]\n' f"\turl = {repo_url}"]) + f.writelines([f'[remote "origin"]\n\turl = {repo_url}']) config = BaseProjectConfig(UniversalConfig()) assert repo_url == config.repo_url @@ -1365,7 +1370,6 @@ def test_orginfo_cache_dir_local(self): ) with TemporaryDirectory() as t: with mock.patch("cumulusci.tests.util.DummyKeychain.cache_dir", Path(t)): - with config.get_orginfo_cache_dir("bar") as directory: assert str(t) in directory, (t, directory) assert ( @@ -1390,9 +1394,9 @@ def test_is_person_accounts_enabled__not_enabled(self): }, "test", ) - assert ( - config._is_person_accounts_enabled is None - ), "_is_person_accounts_enabled should be initialized as None" + assert config._is_person_accounts_enabled is None, ( + "_is_person_accounts_enabled should be initialized as None" + ) responses.add( "GET", @@ -1427,9 +1431,9 @@ def test_is_person_accounts_enabled__is_enabled(self): }, "test", ) - assert ( - config._is_person_accounts_enabled is None - ), "_is_person_accounts_enabled should be initialized as None" + assert config._is_person_accounts_enabled is None, ( + "_is_person_accounts_enabled should be initialized as None" + ) responses.add( "GET", @@ -1464,9 +1468,9 @@ def test_is_multi_currency_enabled__not_enabled(self): }, "test", ) - assert ( - config._multiple_currencies_is_enabled is False - ), "_multiple_currencies_is_enabled should be initialized as False" + assert config._multiple_currencies_is_enabled is False, ( + "_multiple_currencies_is_enabled should be initialized as False" + ) # Login call. responses.add( @@ -1501,21 +1505,21 @@ def test_is_multi_currency_enabled__not_enabled(self): # Check 1: is_multiple_currencies_enabled should be False since the CurrencyType describe gives a 404. actual = config.is_multiple_currencies_enabled - assert ( - actual is False - ), "config.is_multiple_currencies_enabled should be False since the CurrencyType describe returns a 404." - assert ( - config._multiple_currencies_is_enabled is False - ), "config._multiple_currencies_is_enabled should still be False since the CurrencyType describe returns a 404." + assert actual is False, ( + "config.is_multiple_currencies_enabled should be False since the CurrencyType describe returns a 404." + ) + assert config._multiple_currencies_is_enabled is False, ( + "config._multiple_currencies_is_enabled should still be False since the CurrencyType describe returns a 404." + ) # Check 2: We should still get the CurrencyType describe since we never cached that multiple currencies is enabled. actual = config.is_multiple_currencies_enabled - assert ( - actual is False - ), "config.is_multiple_currencies_enabled should be False since the CurrencyType describe returns a 404." - assert ( - config._multiple_currencies_is_enabled is False - ), "config._multiple_currencies_is_enabled should still be False since the CurrencyType describe returns a 404." + assert actual is False, ( + "config.is_multiple_currencies_enabled should be False since the CurrencyType describe returns a 404." + ) + assert config._multiple_currencies_is_enabled is False, ( + "config._multiple_currencies_is_enabled should still be False since the CurrencyType describe returns a 404." + ) # We should have made 3 calls: 1 token call + 2 describe calls assert len(responses.calls) == 1 + 2 @@ -1531,9 +1535,9 @@ def test_is_multi_currency_enabled__is_enabled(self): "test", ) - assert ( - config._multiple_currencies_is_enabled is False - ), "_multiple_currencies_is_enabled should be initialized as False" + assert config._multiple_currencies_is_enabled is False, ( + "_multiple_currencies_is_enabled should be initialized as False" + ) # Token call. responses.add( @@ -1554,21 +1558,21 @@ def test_is_multi_currency_enabled__is_enabled(self): # Check 1: is_multiple_currencies_enabled should be True since the CurrencyType describe gives a 200. actual = config.is_multiple_currencies_enabled - assert ( - actual is True - ), "config.is_multiple_currencies_enabled should be True since the CurrencyType describe returns a 200." - assert ( - config._multiple_currencies_is_enabled is True - ), "config._multiple_currencies_is_enabled should be True since the CurrencyType describe returns a 200." + assert actual is True, ( + "config.is_multiple_currencies_enabled should be True since the CurrencyType describe returns a 200." + ) + assert config._multiple_currencies_is_enabled is True, ( + "config._multiple_currencies_is_enabled should be True since the CurrencyType describe returns a 200." + ) # Check 2: We should have cached that Multiple Currencies is enabled, so we should not make a 2nd descrobe call. This is ok to cache since Multiple Currencies cannot be disabled. actual = config.is_multiple_currencies_enabled - assert ( - actual is True - ), "config.is_multiple_currencies_enabled should be True since the our cached value in _multiple_currencies_is_enabled is True." - assert ( - config._multiple_currencies_is_enabled is True - ), "config._multiple_currencies_is_enabled should still be True." + assert actual is True, ( + "config.is_multiple_currencies_enabled should be True since the our cached value in _multiple_currencies_is_enabled is True." + ) + assert config._multiple_currencies_is_enabled is True, ( + "config._multiple_currencies_is_enabled should still be True." + ) # We should have made 2 calls: 1 token call + 1 describe call assert len(responses.calls) == 1 + 1 @@ -1609,9 +1613,9 @@ def test_is_advanced_currency_management_enabled__multiple_currencies_not_enable # is_advanced_currency_management_enabled should be False since: # - DatedConversionRate describe gives a 404 implying the Sobject is not exposed becuase Multiple Currencies is not enabled. actual = config.is_advanced_currency_management_enabled - assert ( - actual is False - ), "config.is_advanced_currency_management_enabled should be False since the describe gives a 404." + assert actual is False, ( + "config.is_advanced_currency_management_enabled should be False since the describe gives a 404." + ) # We should have made 2 calls: 1 token call + 1 describe call assert len(responses.calls) == 1 + 1 @@ -1649,9 +1653,9 @@ def test_is_advanced_currency_management_enabled__multiple_currencies_enabled__a # - DatedConversionRate describe gives a 200, so the Sobject is exposed (because Multiple Currencies is enabled). # - But DatedConversionRate is not creatable implying ACM is not enabled. actual = config.is_advanced_currency_management_enabled - assert ( - actual is False - ), 'config.is_advanced_currency_management_enabled should be False since though the describe gives a 200, the describe is not "createable".' + assert actual is False, ( + 'config.is_advanced_currency_management_enabled should be False since though the describe gives a 200, the describe is not "createable".' + ) # We should have made 2 calls: 1 token call + 1 describe call assert len(responses.calls) == 1 + 1 @@ -1689,9 +1693,9 @@ def test_is_advanced_currency_management_enabled__multiple_currencies_enabled__a # - DatedConversionRate describe gives a 200, so the Sobject is exposed (because Multiple Currencies is enabled). # - But DatedConversionRate is not creatable implying ACM is not enabled. actual = config.is_advanced_currency_management_enabled - assert ( - actual is True - ), 'config.is_advanced_currency_management_enabled should be False since both the describe gives a 200 and the describe is "createable".' + assert actual is True, ( + 'config.is_advanced_currency_management_enabled should be False since both the describe gives a 200 and the describe is "createable".' + ) # We should have made 2 calls: 1 token call + 1 describe call assert len(responses.calls) == 1 + 1 diff --git a/cumulusci/core/datasets.py b/cumulusci/core/datasets.py index 7ebca75e13..89514fb825 100644 --- a/cumulusci/core/datasets.py +++ b/cumulusci/core/datasets.py @@ -92,9 +92,9 @@ def __exit__(self, *args, **kwargs): self.schema_context.__exit__(*args, **kwargs) # type: ignore def create(self): - assert ( - self.initialized - ), "You must open this context manager. e.g. `with Dataset() as dataset`" + assert self.initialized, ( + "You must open this context manager. e.g. `with Dataset() as dataset`" + ) if not self.path.exists(): self.path.mkdir() diff --git a/cumulusci/core/dependencies/dependencies.py b/cumulusci/core/dependencies/dependencies.py index 2c0050dcba..8d17e7f2a5 100644 --- a/cumulusci/core/dependencies/dependencies.py +++ b/cumulusci/core/dependencies/dependencies.py @@ -56,9 +56,9 @@ def _validate_github_parameters(values): # Populate the `github` property if not already populated. if not values.get("github") and values.get("repo_name"): - values[ - "github" - ] = f"https://github.com/{values['repo_owner']}/{values['repo_name']}" + values["github"] = ( + f"https://github.com/{values['repo_owner']}/{values['repo_name']}" + ) values.pop("repo_owner") values.pop("repo_name") @@ -67,12 +67,10 @@ def _validate_github_parameters(values): class DependencyPin(HashableBaseModel, abc.ABC): @abc.abstractmethod - def can_pin(self, d: "DynamicDependency") -> bool: - ... + def can_pin(self, d: "DynamicDependency") -> bool: ... @abc.abstractmethod - def pin(self, d: "DynamicDependency", context: BaseProjectConfig): - ... + def pin(self, d: "DynamicDependency", context: BaseProjectConfig): ... DependencyPin.update_forward_refs() diff --git a/cumulusci/core/dependencies/resolvers.py b/cumulusci/core/dependencies/resolvers.py index afa0d04e31..8dca8a902b 100644 --- a/cumulusci/core/dependencies/resolvers.py +++ b/cumulusci/core/dependencies/resolvers.py @@ -268,8 +268,7 @@ def get_branches( self, dep: BaseGitHubDependency, context: BaseProjectConfig, - ) -> List[Branch]: - ... + ) -> List[Branch]: ... def resolve( self, dep: BaseGitHubDependency, context: BaseProjectConfig @@ -317,7 +316,8 @@ def is_valid_repo_context(self, context: BaseProjectConfig) -> bool: return bool( super().is_valid_repo_context(context) and is_release_branch_or_child( - context.repo_branch, context.project__git__prefix_feature # type: ignore + context.repo_branch, + context.project__git__prefix_feature, # type: ignore ) ) diff --git a/cumulusci/core/flowrunner.py b/cumulusci/core/flowrunner.py index 098e84ba4d..1cf3756c7d 100644 --- a/cumulusci/core/flowrunner.py +++ b/cumulusci/core/flowrunner.py @@ -1,4 +1,4 @@ -""" FlowRunner contains the logic for actually running a flow. +"""FlowRunner contains the logic for actually running a flow. Flows are an integral part of CCI, they actually *do the thing*. We've been getting along quite nicely with BaseFlow, which turns a flow definition into a callable diff --git a/cumulusci/core/github.py b/cumulusci/core/github.py index eb5732a805..40a3222208 100644 --- a/cumulusci/core/github.py +++ b/cumulusci/core/github.py @@ -189,7 +189,7 @@ def validate_service(options: dict, keychain) -> dict: server_domain = options.get("server_domain", None) gh = _determine_github_client(server_domain, {"token": token}) - if type(gh) == GitHubEnterprise: + if isinstance(gh, GitHubEnterprise): validate_gh_enterprise(server_domain, keychain) try: authed_user = gh.me() @@ -445,7 +445,7 @@ def get_version_id_from_tag(repo: Repository, tag_name: str) -> str: def format_github3_exception( - exc: Union[ResponseError, TransportError, ConnectionError] + exc: Union[ResponseError, TransportError, ConnectionError], ) -> str: """Checks github3 exceptions for the most common GitHub authentication issues, returning a user-friendly message if found. @@ -603,7 +603,7 @@ def catch_common_github_auth_errors(func: Callable) -> Callable: def inner(*args, **kwargs): try: return func(*args, **kwargs) - except (ConnectionError) as exc: + except ConnectionError as exc: if error_msg := format_github3_exception(exc): raise GithubApiError(error_msg) from exc else: diff --git a/cumulusci/core/keychain/base_project_keychain.py b/cumulusci/core/keychain/base_project_keychain.py index 0c1d0a6763..9bd5e647ec 100644 --- a/cumulusci/core/keychain/base_project_keychain.py +++ b/cumulusci/core/keychain/base_project_keychain.py @@ -74,9 +74,9 @@ def create_scratch_org( scratch_config.setdefault("namespaced", False) scratch_config["config_name"] = config_name - scratch_config[ - "sfdx_alias" - ] = f"{self.project_config.project__name}__{org_name}" + scratch_config["sfdx_alias"] = ( + f"{self.project_config.project__name}__{org_name}" + ) org_config = ScratchOrgConfig( scratch_config, org_name, keychain=self, global_org=False ) @@ -353,9 +353,9 @@ def _load_default_connected_app(self): """Load the default connected app as a first class service on the keychain.""" if "connected_app" not in self.config["services"]: self.config["services"]["connected_app"] = {} - self.config["services"]["connected_app"][ - DEFAULT_CONNECTED_APP_NAME - ] = DEFAULT_CONNECTED_APP + self.config["services"]["connected_app"][DEFAULT_CONNECTED_APP_NAME] = ( + DEFAULT_CONNECTED_APP + ) def _set_service( self, service_type, alias, service_config, save=True, config_encrypted=False diff --git a/cumulusci/core/keychain/serialization.py b/cumulusci/core/keychain/serialization.py index 538ed8b73d..51ab2ad2c5 100644 --- a/cumulusci/core/keychain/serialization.py +++ b/cumulusci/core/keychain/serialization.py @@ -128,9 +128,9 @@ def check_round_trip(data: dict, logger: Logger) -> Optional[bytes]: return None try: test_load = load_config_from_json_or_pickle(as_json_text) - assert _simplify_config(test_load) == _simplify_config( - data - ), f"JSON did not round-trip-cleanly {test_load}, {data}" + assert _simplify_config(test_load) == _simplify_config(data), ( + f"JSON did not round-trip-cleanly {test_load}, {data}" + ) except Exception as e: # pragma: no cover report_error("CumulusCI found a problem saving your config:", e, logger) return None diff --git a/cumulusci/core/keychain/tests/test_encrypted_file_project_keychain.py b/cumulusci/core/keychain/tests/test_encrypted_file_project_keychain.py index c761ed6bef..54b808e4ca 100644 --- a/cumulusci/core/keychain/tests/test_encrypted_file_project_keychain.py +++ b/cumulusci/core/keychain/tests/test_encrypted_file_project_keychain.py @@ -67,7 +67,6 @@ def keychain(project_config, key) -> EncryptedFileProjectKeychain: class TestEncryptedFileProjectKeychain: - project_name = "TestProject" def _write_file(self, filepath, contents): @@ -138,7 +137,6 @@ def test_set_org__should_not_save_when_environment_project_keychain_set( self, keychain, org_config, withdifferentformats ): with temporary_dir() as temp: - env = EnvironmentVarGuard() with EnvironmentVarGuard() as env: env.set("CUMULUSCI_KEYCHAIN_CLASS", "EnvironmentProjectKeychain") with mock.patch.object( @@ -281,7 +279,6 @@ def test_get_default_org__outside_project(self, keychain): def test_load_orgs_from_environment(self, keychain, org_config): scratch_config = org_config.config.copy() scratch_config["scratch"] = True - env = EnvironmentVarGuard() with EnvironmentVarGuard() as env: env.set( f"{keychain.env_org_var_prefix}dev", @@ -299,7 +296,6 @@ def test_load_orgs_from_environment(self, keychain, org_config): assert _simplify_config(actual_config.config) == org_config.config def test_load_orgs_from_environment__empty_throws_error(self, keychain, org_config): - env = EnvironmentVarGuard() with EnvironmentVarGuard() as env: env.set( f"{keychain.env_org_var_prefix}dev", @@ -311,7 +307,6 @@ def test_load_orgs_from_environment__empty_throws_error(self, keychain, org_conf def test_load_orgs_from_environment__invalid_json_throws_error( self, keychain, org_config ): - env = EnvironmentVarGuard() with EnvironmentVarGuard() as env: env.set( f"{keychain.env_org_var_prefix}dev", @@ -384,7 +379,6 @@ def test_load_services_from_env__same_name_throws_error(self, keychain): def test_load_services_from_env__empty_throws_error(self, keychain): service_prefix = EncryptedFileProjectKeychain.env_service_var_prefix - env = EnvironmentVarGuard() with EnvironmentVarGuard() as env: env.set( f"{service_prefix}github", @@ -395,7 +389,6 @@ def test_load_services_from_env__empty_throws_error(self, keychain): def test_load_services_from_env__invalid_json_throws_error(self, keychain): service_prefix = EncryptedFileProjectKeychain.env_service_var_prefix - env = EnvironmentVarGuard() with EnvironmentVarGuard() as env: env.set( f"{service_prefix}github", diff --git a/cumulusci/core/source_transforms/tests/test_transforms.py b/cumulusci/core/source_transforms/tests/test_transforms.py index 619e9a125e..1cd3ddc90f 100644 --- a/cumulusci/core/source_transforms/tests/test_transforms.py +++ b/cumulusci/core/source_transforms/tests/test_transforms.py @@ -152,7 +152,7 @@ def test_namespace_injection_ignores_binary(task_context): ZipFileSpec( { Path("ns__Foo.cls"): "System.debug('ns__blah');", - Path("b.staticResource"): b"ns__\xFF\xFF", + Path("b.staticResource"): b"ns__\xff\xff", } ).as_zipfile(), options={"namespace_tokenize": "ns", "unmanaged": False}, @@ -162,7 +162,7 @@ def test_namespace_injection_ignores_binary(task_context): ZipFileSpec( { Path("___NAMESPACE___Foo.cls"): "System.debug('%%%NAMESPACE%%%blah');", - Path("b.staticResource"): b"ns__\xFF\xFF", + Path("b.staticResource"): b"ns__\xff\xff", } ) == builder.zf diff --git a/cumulusci/core/source_transforms/transforms.py b/cumulusci/core/source_transforms/transforms.py index 9bf0499a8e..1ec5f2d28f 100644 --- a/cumulusci/core/source_transforms/transforms.py +++ b/cumulusci/core/source_transforms/transforms.py @@ -34,12 +34,10 @@ class SourceTransform(abc.ABC): options_model: T.Optional[T.Type[BaseModel]] identifier: str - def __init__(self): - ... + def __init__(self): ... @abc.abstractmethod - def process(self, zf: ZipFile, context: TaskContext) -> ZipFile: - ... + def process(self, zf: ZipFile, context: TaskContext) -> ZipFile: ... class SourceTransformSpec(BaseModel): @@ -314,8 +312,7 @@ def validate_find_xpath(cls, values): return values @abc.abstractmethod - def get_replace_string(self, context: TaskContext) -> str: - ... + def get_replace_string(self, context: TaskContext) -> str: ... class FindReplaceSpec(FindReplaceBaseSpec): diff --git a/cumulusci/core/tasks.py b/cumulusci/core/tasks.py index 7c8cffc2fb..5d7b4c6065 100644 --- a/cumulusci/core/tasks.py +++ b/cumulusci/core/tasks.py @@ -1,7 +1,8 @@ -""" Tasks are the basic unit of execution in CumulusCI. +"""Tasks are the basic unit of execution in CumulusCI. Subclass BaseTask or a descendant to define custom task logic """ + import contextlib import logging import os diff --git a/cumulusci/core/tests/fake_remote_repo/tasks/example.py b/cumulusci/core/tests/fake_remote_repo/tasks/example.py index 48b0bdc685..b70e688050 100644 --- a/cumulusci/core/tests/fake_remote_repo/tasks/example.py +++ b/cumulusci/core/tests/fake_remote_repo/tasks/example.py @@ -8,7 +8,6 @@ def _run_task(self): class StaticPreflightTask(BaseTask): - task_options = { "task_name": { "description": "Task that this preflight is for", @@ -35,7 +34,7 @@ class StaticSleep(Sleep): "description": "Task that this preflight is for", "required": True, }, - } + }, ) def _run_task(self): diff --git a/cumulusci/core/tests/test_datasets_e2e.py b/cumulusci/core/tests/test_datasets_e2e.py index 387ad696ad..6b5399d9ec 100644 --- a/cumulusci/core/tests/test_datasets_e2e.py +++ b/cumulusci/core/tests/test_datasets_e2e.py @@ -33,14 +33,17 @@ def setup_test(org_config): describe_for("Contact"), describe_for("Opportunity"), ) - with patch.object(type(org_config), "is_person_accounts_enabled", False), patch( - "cumulusci.core.datasets.get_org_schema", - lambda _sf, org_config, **kwargs: _fake_get_org_schema( - org_config, - obj_describes, - object_counts, - included_objects=["Account", "Contact", "Opportunity"], - **kwargs, + with ( + patch.object(type(org_config), "is_person_accounts_enabled", False), + patch( + "cumulusci.core.datasets.get_org_schema", + lambda _sf, org_config, **kwargs: _fake_get_org_schema( + org_config, + obj_describes, + object_counts, + included_objects=["Account", "Contact", "Opportunity"], + **kwargs, + ), ), ): yield @@ -76,20 +79,19 @@ def test_datasets_e2e( describe_for("Opportunity"), ) - with patch.object( - type(org_config), "is_person_accounts_enabled", False - ), _fake_get_org_schema( - org_config, - obj_describes, - object_counts, - include_counts=True, - filters=[Filters.extractable, Filters.createable], - included_objects=["Account", "Contact", "Opportunity"], - ) as schema, ensure_accounts( - 6 - ), Dataset( - "foo", project_config, sf, org_config, schema=schema - ) as dataset: + with ( + patch.object(type(org_config), "is_person_accounts_enabled", False), + _fake_get_org_schema( + org_config, + obj_describes, + object_counts, + include_counts=True, + filters=[Filters.extractable, Filters.createable], + included_objects=["Account", "Contact", "Opportunity"], + ) as schema, + ensure_accounts(6), + Dataset("foo", project_config, sf, org_config, schema=schema) as dataset, + ): timer.checkpoint("In Dataset") if dataset.path.exists(): rmtree(dataset.path) @@ -183,18 +185,21 @@ def test_datasets_extract_standard_objects( # Need record types for the RecordTypeId field to be in the org create_record_type_for_account(sf, run_code_without_recording) - with patch.object(type(org_config), "is_person_accounts_enabled", False), patch( - "cumulusci.core.datasets.get_org_schema", - lambda _sf, org_config, **kwargs: _fake_get_org_schema( - org_config, - obj_describes, - object_counts, - included_objects=["Account", "Contact", "Opportunity"], - **kwargs, + with ( + patch.object(type(org_config), "is_person_accounts_enabled", False), + patch( + "cumulusci.core.datasets.get_org_schema", + lambda _sf, org_config, **kwargs: _fake_get_org_schema( + org_config, + obj_describes, + object_counts, + included_objects=["Account", "Contact", "Opportunity"], + **kwargs, + ), ), - ), ensure_accounts(6), Dataset( - "bar", project_config, sf, org_config - ) as dataset: + ensure_accounts(6), + Dataset("bar", project_config, sf, org_config) as dataset, + ): timer.checkpoint("In Dataset") if dataset.path.exists(): rmtree(dataset.path) @@ -240,18 +245,21 @@ def test_datasets_read_explicit_extract_declaration( describe_for("Lead"), describe_for("Event"), ) - with patch.object(type(org_config), "is_person_accounts_enabled", False), patch( - "cumulusci.core.datasets.get_org_schema", - lambda _sf, org_config, **kwargs: _fake_get_org_schema( - org_config, - obj_describes, - object_counts, - included_objects=["Account", "Contact", "Opportunity"], - **kwargs, + with ( + patch.object(type(org_config), "is_person_accounts_enabled", False), + patch( + "cumulusci.core.datasets.get_org_schema", + lambda _sf, org_config, **kwargs: _fake_get_org_schema( + org_config, + obj_describes, + object_counts, + included_objects=["Account", "Contact", "Opportunity"], + **kwargs, + ), ), - ), ensure_accounts(6), Dataset( - "bar", project_config, sf, org_config - ) as dataset: + ensure_accounts(6), + Dataset("bar", project_config, sf, org_config) as dataset, + ): if dataset.path.exists(): rmtree(dataset.path) @@ -350,21 +358,24 @@ def fake_run_snowfakery(self): assert "foo.recipe.yml" in self.options["recipe"] called = True - with setup_test(org_config), Dataset( - "foo", project_config, sf, org_config, schema=None - ) as dataset, patch( - "cumulusci.core.datasets.Path.exists", fake_path_exists - ), patch( - "cumulusci.tasks.bulkdata.snowfakery.Snowfakery._run_task", - fake_run_snowfakery, + with ( + setup_test(org_config), + Dataset("foo", project_config, sf, org_config, schema=None) as dataset, + patch("cumulusci.core.datasets.Path.exists", fake_path_exists), + patch( + "cumulusci.tasks.bulkdata.snowfakery.Snowfakery._run_task", + fake_run_snowfakery, + ), ): dataset.load() assert called def test_dataset_with_no_data_or_recipe(self, sf, project_config, org_config): - with setup_test(org_config), Dataset( - "fxoyoxz", project_config, sf, org_config, schema=None - ) as dataset, pytest.raises(BulkDataException, match="fxoyoxz"): + with ( + setup_test(org_config), + Dataset("fxoyoxz", project_config, sf, org_config, schema=None) as dataset, + pytest.raises(BulkDataException, match="fxoyoxz"), + ): dataset.load() diff --git a/cumulusci/core/tests/test_flowrunner.py b/cumulusci/core/tests/test_flowrunner.py index 40c9257ffd..6b99a5cac2 100644 --- a/cumulusci/core/tests/test_flowrunner.py +++ b/cumulusci/core/tests/test_flowrunner.py @@ -780,9 +780,10 @@ def include_fake_project(self: BaseProjectConfig, _spec) -> BaseProjectConfig: def test_cross_project_tasks(get_tempfile_logger): # get_tempfile_logger doesn't clean up after itself which breaks other tests get_tempfile_logger.return_value = mock.Mock(), "" - with mock.patch("cumulusci.core.debug._DEBUG_MODE", get=lambda: True), mock.patch( - "logging.Logger.info", wraps=lambda data: print(data) - ) as out: + with ( + mock.patch("cumulusci.core.debug._DEBUG_MODE", get=lambda: True), + mock.patch("logging.Logger.info", wraps=lambda data: print(data)) as out, + ): cci.main( [ "cci", diff --git a/cumulusci/core/tests/test_github.py b/cumulusci/core/tests/test_github.py index 23976bf0c8..64203cc92e 100644 --- a/cumulusci/core/tests/test_github.py +++ b/cumulusci/core/tests/test_github.py @@ -280,7 +280,7 @@ def test_get_auth_from_service(self, keychain_enterprise): ) def test_determine_github_client(self, domain, client): client_result = _determine_github_client(domain, {}) - assert type(client_result) == client + assert isinstance(client_result, client) @responses.activate def test_get_pull_requests_by_head(self, mock_util, repo): diff --git a/cumulusci/core/tests/test_tasks.py b/cumulusci/core/tests/test_tasks.py index 38535a8c46..e4c1a7fd61 100644 --- a/cumulusci/core/tests/test_tasks.py +++ b/cumulusci/core/tests/test_tasks.py @@ -1,4 +1,4 @@ -""" Tests for the CumulusCI task module """ +"""Tests for the CumulusCI task module""" import collections import logging diff --git a/cumulusci/core/tests/utils.py b/cumulusci/core/tests/utils.py index 0a0dc55ace..7a08654f76 100644 --- a/cumulusci/core/tests/utils.py +++ b/cumulusci/core/tests/utils.py @@ -1,4 +1,4 @@ -""" Utilities for testing CumulusCI +"""Utilities for testing CumulusCI MockLoggingHandler: a logging handler that we can assert""" @@ -47,7 +47,6 @@ def reset(self): class EnvironmentVarGuard(collections.abc.MutableMapping): - """Class to help protect the environment variable properly. Can be used as a context manager.""" @@ -90,7 +89,7 @@ def __enter__(self): return self def __exit__(self, *ignore_exc): - for (k, v) in self._changed.items(): + for k, v in self._changed.items(): if v is None: if k in self._environ: del self._environ[k] diff --git a/cumulusci/core/utils.py b/cumulusci/core/utils.py index 88cd570657..4745a18528 100644 --- a/cumulusci/core/utils.py +++ b/cumulusci/core/utils.py @@ -1,4 +1,4 @@ -""" Utilities for CumulusCI Core""" +"""Utilities for CumulusCI Core""" import copy import glob diff --git a/cumulusci/oauth/client.py b/cumulusci/oauth/client.py index 91059eba15..2f6fea8113 100644 --- a/cumulusci/oauth/client.py +++ b/cumulusci/oauth/client.py @@ -201,8 +201,7 @@ def _create_httpd(self): keyfile = "key.pem" if not Path(certfile).is_file() or not Path(keyfile).is_file(): create_key_and_self_signed_cert() - # FIXME: Use ssl.PROTOCOL_TLS_SERVER after dropping 3.8 support - ssl_context = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS) + ssl_context = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS_SERVER) ssl_context.load_cert_chain(certfile, keyfile) httpd.socket = ssl_context.wrap_socket( httpd.socket, diff --git a/cumulusci/oauth/tests/test_client.py b/cumulusci/oauth/tests/test_client.py index 430b18e9ab..c342fdc275 100644 --- a/cumulusci/oauth/tests/test_client.py +++ b/cumulusci/oauth/tests/test_client.py @@ -92,9 +92,9 @@ def run_code_and_check_exception(): break time.sleep(0.01) - assert ( - oauth_client.httpd - ), "HTTPD did not start. Perhaps port 8080 cannot be accessed." + assert oauth_client.httpd, ( + "HTTPD did not start. Perhaps port 8080 cannot be accessed." + ) try: yield oauth_client diff --git a/cumulusci/robotframework/SalesforceAPI.py b/cumulusci/robotframework/SalesforceAPI.py index 588880182e..a257a1f7d9 100644 --- a/cumulusci/robotframework/SalesforceAPI.py +++ b/cumulusci/robotframework/SalesforceAPI.py @@ -211,9 +211,9 @@ def salesforce_collection_insert(self, objects): | Salesforce Collection Insert ${objects} """ - assert ( - not obj.get("id", None) for obj in objects - ), "Insertable objects should not have IDs" + assert (not obj.get("id", None) for obj in objects), ( + "Insertable objects should not have IDs" + ) assert len(objects) <= SF_COLLECTION_INSERTION_LIMIT, ( "Cannot insert more than %s objects with this keyword" % SF_COLLECTION_INSERTION_LIMIT @@ -261,9 +261,9 @@ def salesforce_collection_update(self, objects): """ for obj in objects: - assert obj[ - "id" - ], "Should be a list of objects with Ids returned by Salesforce Collection Insert" + assert obj["id"], ( + "Should be a list of objects with Ids returned by Salesforce Collection Insert" + ) if STATUS_KEY in obj: del obj[STATUS_KEY] diff --git a/cumulusci/robotframework/SalesforcePlaywright.py b/cumulusci/robotframework/SalesforcePlaywright.py index 9eba2edf11..2302d1513b 100644 --- a/cumulusci/robotframework/SalesforcePlaywright.py +++ b/cumulusci/robotframework/SalesforcePlaywright.py @@ -207,7 +207,7 @@ def _check_for_classic(self): self.browser.click("a.switch-to-lightning") return True - except (AssertionError): + except AssertionError: return False def breakpoint(self): diff --git a/cumulusci/robotframework/locator_manager.py b/cumulusci/robotframework/locator_manager.py index 8a0de6eafb..e3a0ae8a58 100644 --- a/cumulusci/robotframework/locator_manager.py +++ b/cumulusci/robotframework/locator_manager.py @@ -69,7 +69,7 @@ def add_location_strategies(): # exists, so we use a flag to make sure this code is called # only once. selenium = BuiltIn().get_library_instance("SeleniumLibrary") - for (prefix, strategy) in LOCATORS.items(): + for prefix, strategy in LOCATORS.items(): try: logger.debug(f"adding location strategy for '{prefix}'") selenium.add_location_strategy( diff --git a/cumulusci/robotframework/locators_56.py b/cumulusci/robotframework/locators_56.py index e6dd19ba8d..4060cf0d33 100644 --- a/cumulusci/robotframework/locators_56.py +++ b/cumulusci/robotframework/locators_56.py @@ -16,7 +16,7 @@ "div.desktop.container.oneOne.oneAppLayoutHost[data-aura-rendered-by]", "list_view_menu": { "button": "css:button[title='List View Controls']", - "item": "//div[@title='List View " "Controls']//ul[@role='menu']//li/a[.='{}']", + "item": "//div[@title='List View Controls']//ul[@role='menu']//li/a[.='{}']", }, "loading_box": "css: div.auraLoadingBox.oneLoadingBox", "modal": { @@ -27,8 +27,7 @@ "field_alert": "//div[contains(@class, 'forceFormPageError')]", "has_error": "css: div.forceFormPageError", "is_open": "css: div.uiModal div.panel.slds-modal", - "review_alert": "//a[@records-recordediterror_recordediterror " - "and text()='{}']", + "review_alert": "//a[@records-recordediterror_recordediterror and text()='{}']", }, "object": { "button": "//div[contains(@class, " diff --git a/cumulusci/robotframework/pageobjects/ObjectManagerPageObject.py b/cumulusci/robotframework/pageobjects/ObjectManagerPageObject.py index c0fc46a49a..a355e23d4f 100644 --- a/cumulusci/robotframework/pageobjects/ObjectManagerPageObject.py +++ b/cumulusci/robotframework/pageobjects/ObjectManagerPageObject.py @@ -203,7 +203,7 @@ def delete_custom_field(self, field_name): except Exception as e: self.builtin.log( - f"on try #{tries+1} we caught this error: {e}", "DEBUG" + f"on try #{tries + 1} we caught this error: {e}", "DEBUG" ) self.builtin.sleep("1 second") last_error = e diff --git a/cumulusci/robotframework/tests/CustomObjectTestPage.py b/cumulusci/robotframework/tests/CustomObjectTestPage.py index d91903929b..5bd54b6965 100644 --- a/cumulusci/robotframework/tests/CustomObjectTestPage.py +++ b/cumulusci/robotframework/tests/CustomObjectTestPage.py @@ -1,6 +1,7 @@ """ This class is used by test_pageobjects """ + from cumulusci.robotframework.pageobjects import ListingPage, pageobject diff --git a/cumulusci/robotframework/tests/salesforce/TestLibraryA.py b/cumulusci/robotframework/tests/salesforce/TestLibraryA.py index bbbda7aeeb..6f42d3023d 100644 --- a/cumulusci/robotframework/tests/salesforce/TestLibraryA.py +++ b/cumulusci/robotframework/tests/salesforce/TestLibraryA.py @@ -2,6 +2,7 @@ This is a library used by locators.robot for testing custom locator strategies """ + from cumulusci.robotframework.locator_manager import ( register_locators, translate_locator, diff --git a/cumulusci/robotframework/tests/salesforce/TestLibraryB.py b/cumulusci/robotframework/tests/salesforce/TestLibraryB.py index fcea759f1f..271411706b 100644 --- a/cumulusci/robotframework/tests/salesforce/TestLibraryB.py +++ b/cumulusci/robotframework/tests/salesforce/TestLibraryB.py @@ -2,6 +2,7 @@ This is a library used by locators.robot for testing custom locator strategies """ + from cumulusci.robotframework.locator_manager import ( register_locators, translate_locator, diff --git a/cumulusci/robotframework/tests/salesforce/TestListener.py b/cumulusci/robotframework/tests/salesforce/TestListener.py index 0894da4ceb..7c69e7c689 100644 --- a/cumulusci/robotframework/tests/salesforce/TestListener.py +++ b/cumulusci/robotframework/tests/salesforce/TestListener.py @@ -1,15 +1,16 @@ """This hybrid library/listener can be used to verify messages that - have been logged and keywords have been called. +have been logged and keywords have been called. - This works by listening for log messages and keywords via the - listener interface, and saving them in a cache. Keywords are - provided for doing assertions on called keywords and for resetting - the cache. +This works by listening for log messages and keywords via the +listener interface, and saving them in a cache. Keywords are +provided for doing assertions on called keywords and for resetting +the cache. - The keyword cache is reset for each test case to help keep it - from growing too large. +The keyword cache is reset for each test case to help keep it +from growing too large. """ + import re diff --git a/cumulusci/robotframework/tests/salesforce/labels.html b/cumulusci/robotframework/tests/salesforce/labels.html index c1b8bde22e..8c9190df91 100644 --- a/cumulusci/robotframework/tests/salesforce/labels.html +++ b/cumulusci/robotframework/tests/salesforce/labels.html @@ -1,4 +1,4 @@ - + Labels for testing diff --git a/cumulusci/robotframework/tests/test_cumulusci_library.py b/cumulusci/robotframework/tests/test_cumulusci_library.py index ff17b2b190..51639a979a 100644 --- a/cumulusci/robotframework/tests/test_cumulusci_library.py +++ b/cumulusci/robotframework/tests/test_cumulusci_library.py @@ -72,9 +72,9 @@ def test_robot_logger_supports_warning(self): self.cumulusci.run_task("get_pwd") args, kwargs = self.cumulusci._run_task.call_args task = args[0] - assert hasattr( - task.logger, "warning" - ), "robot logger should have a warning method but doesn't" + assert hasattr(task.logger, "warning"), ( + "robot logger should have a warning method but doesn't" + ) def test_robot_logger_supports_log(self): """Verify that 'run task' uses a logger that supports .log() @@ -89,7 +89,6 @@ def test_robot_logger_supports_log(self): args, kwargs = self.cumulusci._run_task.call_args task = args[0] with mock.patch.object(task.logger, "write") as logger_write: - task.logger.log(logging.CRITICAL, "a critical message") task.logger.log(logging.ERROR, "an error message") task.logger.log(logging.WARN, "a warning message") @@ -269,7 +268,6 @@ def test_login_url_user_org_with_get_access_token(self): with mock.patch.object( self.cumulusci.org, "get_access_token", return_value="super-secret-token" ): - url = self.cumulusci.login_url(username="test@example.com") self.cumulusci.org.get_access_token.assert_called_once_with( username="test@example.com" diff --git a/cumulusci/robotframework/tests/test_pageobjects.py b/cumulusci/robotframework/tests/test_pageobjects.py index 3f522d1467..ae6cb47f5a 100644 --- a/cumulusci/robotframework/tests/test_pageobjects.py +++ b/cumulusci/robotframework/tests/test_pageobjects.py @@ -182,7 +182,6 @@ def test_namespaced_object_name(self, get_context_mock, get_library_instance_moc CumulusCI, "get_namespace_prefix", return_value="foobar__" ): with reload_PageObjects(FOO_PATH) as po: - FooTestPage = importer.import_class_or_module_by_path(FOO_PATH) MockGetLibraryInstance.libs["FooTestPage"] = _PageObjectLibrary( FooTestPage() @@ -197,7 +196,6 @@ def test_non_namespaced_object_name( """Verify that the object name is not prefixed by a namespace when there is no namespace""" with mock.patch.object(CumulusCI, "get_namespace_prefix", return_value=""): with reload_PageObjects(FOO_PATH) as po: - FooTestPage = importer.import_class_or_module_by_path(FOO_PATH) MockGetLibraryInstance.libs["FooTestPage"] = _PageObjectLibrary( FooTestPage() diff --git a/cumulusci/robotframework/tests/test_salesforce_locators.py b/cumulusci/robotframework/tests/test_salesforce_locators.py index 111c1e1173..0a706b7fe0 100644 --- a/cumulusci/robotframework/tests/test_salesforce_locators.py +++ b/cumulusci/robotframework/tests/test_salesforce_locators.py @@ -66,8 +66,8 @@ def test_locators_57(self): keys_56 = set(locators_56.lex_locators) keys_57 = set(locators_57.lex_locators) - assert id(locators_56.lex_locators) != id( - locators_57.lex_locators - ), "locators_56.lex_locators and locators_57.lex_locators are the same object" + assert id(locators_56.lex_locators) != id(locators_57.lex_locators), ( + "locators_56.lex_locators and locators_57.lex_locators are the same object" + ) assert len(keys_56) > 0 assert keys_57.issubset(keys_56) diff --git a/cumulusci/robotframework/utils.py b/cumulusci/robotframework/utils.py index 299545d1d0..513ef6b7a2 100644 --- a/cumulusci/robotframework/utils.py +++ b/cumulusci/robotframework/utils.py @@ -72,7 +72,6 @@ def set_pdb_trace(pm=False): # pragma: no cover class RetryingSeleniumLibraryMixin(object): - debug = False @property diff --git a/cumulusci/salesforce_api/org_schema.py b/cumulusci/salesforce_api/org_schema.py index 2dd27a2844..e10f9f2f17 100644 --- a/cumulusci/salesforce_api/org_schema.py +++ b/cumulusci/salesforce_api/org_schema.py @@ -151,6 +151,7 @@ def get(self, name: str): def block_writing(self): """After this method is called, the database can't be updated again""" + # changes don't get saved back to the gzip # so there is no point writing to the DB def closed(): @@ -232,8 +233,7 @@ def _populate_cache_from_describe(self, describe_objs: List["DescribeUpdate"]): metadata.reflect() with BufferedSession(engine, metadata) as sess: - - for (sobj_data, last_modified) in describe_objs: + for sobj_data, last_modified in describe_objs: sobj_data = sobj_data.copy() fields = sobj_data.pop("fields") sobj_data["last_modified_date"] = last_modified diff --git a/cumulusci/salesforce_api/rest_deploy.py b/cumulusci/salesforce_api/rest_deploy.py index 70d532569a..9276f42db2 100644 --- a/cumulusci/salesforce_api/rest_deploy.py +++ b/cumulusci/salesforce_api/rest_deploy.py @@ -138,7 +138,7 @@ def _reformat_zip(self, package_zip): # Construct an error message from deployment failure details def _construct_error_message(self, failure): - error_message = f"{str.upper(failure['problemType'])} in file {failure['fileName'][len(PARENT_DIR_NAME)+len('/'):]}: {failure['problem']}" + error_message = f"{str.upper(failure['problemType'])} in file {failure['fileName'][len(PARENT_DIR_NAME) + len('/') :]}: {failure['problem']}" if failure["lineNumber"] and failure["columnNumber"]: error_message += ( diff --git a/cumulusci/salesforce_api/retrieve_profile_api.py b/cumulusci/salesforce_api/retrieve_profile_api.py index 72aa2c963f..3ffda70473 100644 --- a/cumulusci/salesforce_api/retrieve_profile_api.py +++ b/cumulusci/salesforce_api/retrieve_profile_api.py @@ -266,7 +266,7 @@ def _process_setupEntityAccess_results(self, result_list: List[dict]): and item["NamespacePrefix"] is not None ): extracted_values[data.package_xml_name].append( - f'{item["NamespacePrefix"]}__{item[data.columns[0]]}' + f"{item['NamespacePrefix']}__{item[data.columns[0]]}" ) else: extracted_values[data.package_xml_name].append( diff --git a/cumulusci/salesforce_api/tests/test_package_zip.py b/cumulusci/salesforce_api/tests/test_package_zip.py index fe4a85bbd3..fa27b78b5b 100644 --- a/cumulusci/salesforce_api/tests/test_package_zip.py +++ b/cumulusci/salesforce_api/tests/test_package_zip.py @@ -37,7 +37,6 @@ def test_as_hash(self): class TestMetadataPackageZipBuilder: def test_builder(self, task_context): with temporary_dir() as path: - # add package.xml with open(os.path.join(path, "package.xml"), "w") as f: f.write( diff --git a/cumulusci/salesforce_api/tests/test_retrieve_profile_api.py b/cumulusci/salesforce_api/tests/test_retrieve_profile_api.py index 99cc67eef3..bd16bfcf6b 100644 --- a/cumulusci/salesforce_api/tests/test_retrieve_profile_api.py +++ b/cumulusci/salesforce_api/tests/test_retrieve_profile_api.py @@ -38,9 +38,10 @@ def test_init_task(retrieve_profile_api_instance): def test_retrieve_existing_profiles(retrieve_profile_api_instance): profiles = ["Profile1", "Profile2", "Admin"] result = {"records": [{"Name": "Profile1"}]} - with patch.object( - RetrieveProfileApi, "_build_query", return_value="some_query" - ), patch.object(RetrieveProfileApi, "_run_query", return_value=result): + with ( + patch.object(RetrieveProfileApi, "_build_query", return_value="some_query"), + patch.object(RetrieveProfileApi, "_run_query", return_value=result), + ): existing_profiles = retrieve_profile_api_instance._retrieve_existing_profiles( profiles ) @@ -174,14 +175,18 @@ def test_process_setupEntityAccess_results(retrieve_profile_api_instance): "ApexPage": [{"Id": "002def", "Name": "TestApexPage"}], "CustomPermission": [], } - with patch.object( - RetrieveProfileApi, "_build_query", return_value="SELECT Id, Name FROM Table" - ) as mock_build_query, patch.object( - RunParallelQueries, - "_run_queries_in_parallel", - return_value=queries_result, - ) as mock_run_queries: - + with ( + patch.object( + RetrieveProfileApi, + "_build_query", + return_value="SELECT Id, Name FROM Table", + ) as mock_build_query, + patch.object( + RunParallelQueries, + "_run_queries_in_parallel", + return_value=queries_result, + ) as mock_run_queries, + ): ( entities, result, @@ -222,29 +227,34 @@ def test_process_all_results(retrieve_profile_api_instance): "customTab": "some_result", "profileFlow": "some_result", } - with patch.object( - RetrieveProfileApi, - "_process_setupEntityAccess_results", - return_value=( - { - "ApexClass": ["TestApexClass"], - "ApexPage": ["TestApexPage"], - "FlowDefinition": ["TestFlow"], - }, - {"FlowDefinition": ["some_result"]}, + with ( + patch.object( + RetrieveProfileApi, + "_process_setupEntityAccess_results", + return_value=( + { + "ApexClass": ["TestApexClass"], + "ApexPage": ["TestApexPage"], + "FlowDefinition": ["TestFlow"], + }, + {"FlowDefinition": ["some_result"]}, + ), + ), + patch.object( + RetrieveProfileApi, + "_process_sObject_results", + return_value={"CustomObject": ["TestObject"]}, + ), + patch.object( + RetrieveProfileApi, + "_process_customTab_results", + return_value={"CustomTab": ["TestTab"]}, + ), + patch.object( + RetrieveProfileApi, + "_match_profiles_and_flows", + return_value={"Profile1": ["Flow1"]}, ), - ), patch.object( - RetrieveProfileApi, - "_process_sObject_results", - return_value={"CustomObject": ["TestObject"]}, - ), patch.object( - RetrieveProfileApi, - "_process_customTab_results", - return_value={"CustomTab": ["TestTab"]}, - ), patch.object( - RetrieveProfileApi, - "_match_profiles_and_flows", - return_value={"Profile1": ["Flow1"]}, ): entities, profile_flow = retrieve_profile_api_instance._process_all_results( result_dict @@ -291,16 +301,19 @@ def test_retrieve_permissionable_entities(retrieve_profile_api_instance): {"Profile1": ["Flow1"]}, ) - with patch.object( - RunParallelQueries, "_run_queries_in_parallel" - ) as mock_run_queries, patch.object( - RetrieveProfileApi, - "_queries_retrieve_permissions", - return_value=expected_queries, - ), patch.object( - RetrieveProfileApi, "_process_all_results", return_value=expected_result + with ( + patch.object( + RunParallelQueries, "_run_queries_in_parallel" + ) as mock_run_queries, + patch.object( + RetrieveProfileApi, + "_queries_retrieve_permissions", + return_value=expected_queries, + ), + patch.object( + RetrieveProfileApi, "_process_all_results", return_value=expected_result + ), ): - result = retrieve_profile_api_instance._retrieve_permissionable_entities( profiles ) diff --git a/cumulusci/tasks/apex/batch.py b/cumulusci/tasks/apex/batch.py index d10fad278e..fcb1732d40 100644 --- a/cumulusci/tasks/apex/batch.py +++ b/cumulusci/tasks/apex/batch.py @@ -1,4 +1,5 @@ -""" a task for waiting on a Batch Apex job to complete """ +"""a task for waiting on a Batch Apex job to complete""" + from datetime import datetime from typing import Optional, Sequence diff --git a/cumulusci/tasks/apex/testrunner.py b/cumulusci/tasks/apex/testrunner.py index 5a51f655ed..e299ee1388 100644 --- a/cumulusci/tasks/apex/testrunner.py +++ b/cumulusci/tasks/apex/testrunner.py @@ -1,4 +1,4 @@ -""" CumulusCI Tasks for running Apex Tests """ +"""CumulusCI Tasks for running Apex Tests""" import html import io @@ -264,7 +264,6 @@ def _init_class(self): def _get_namespace_filter(self): if self.options.get("managed"): - namespace = self.options.get("namespace") if not namespace: @@ -430,9 +429,9 @@ def _get_test_results(self, allow_retries=True): for test_result in result["records"]: class_name = self.classes_by_id[test_result["ApexClassId"]] - self.results_by_class_name[class_name][ - test_result["MethodName"] - ] = test_result + self.results_by_class_name[class_name][test_result["MethodName"]] = ( + test_result + ) self.counts[test_result["Outcome"]] += 1 # If we have class-level failures that did not come with line-level diff --git a/cumulusci/tasks/bulkdata/extract.py b/cumulusci/tasks/bulkdata/extract.py index adcaa0f4e1..95b31dc485 100644 --- a/cumulusci/tasks/bulkdata/extract.py +++ b/cumulusci/tasks/bulkdata/extract.py @@ -95,7 +95,6 @@ def _init_db(self): self.models = {} with self._database_url() as database_url: - # initialize the DB engine parent_engine = create_engine(database_url) with parent_engine.connect() as connection: diff --git a/cumulusci/tasks/bulkdata/extract_dataset_utils/extract_yml.py b/cumulusci/tasks/bulkdata/extract_dataset_utils/extract_yml.py index 1ece5c1cd7..af84f728bd 100644 --- a/cumulusci/tasks/bulkdata/extract_dataset_utils/extract_yml.py +++ b/cumulusci/tasks/bulkdata/extract_dataset_utils/extract_yml.py @@ -43,9 +43,9 @@ def parse_field_complex_type(fieldspec): def assert_sf_object_fits_pattern(self): if self.is_group: - assert ( - self.group_type in SFObjectGroupTypes - ), f"Expected OBJECTS(ALL), OBJECTS(CUSTOM) or OBJECTS(STANDARD), not `{self.group_type.upper()}`" + assert self.group_type in SFObjectGroupTypes, ( + f"Expected OBJECTS(ALL), OBJECTS(CUSTOM) or OBJECTS(STANDARD), not `{self.group_type.upper()}`" + ) else: assert self.sf_object.isidentifier(), ( "Value should start with OBJECTS( or be a simple alphanumeric field name" @@ -55,9 +55,9 @@ def assert_sf_object_fits_pattern(self): def assert_check_where_against_complex(self): """Check that a where clause was not used with a group declaration.""" - assert not ( - self.where and self.is_group - ), "Cannot specify a `where` clause on a declaration for multiple kinds of objects." + assert not (self.where and self.is_group), ( + "Cannot specify a `where` clause on a declaration for multiple kinds of objects." + ) @validator("fields_") def normalize_fields(cls, vals): diff --git a/cumulusci/tasks/bulkdata/extract_dataset_utils/tests/test_synthesize_extract_declarations.py b/cumulusci/tasks/bulkdata/extract_dataset_utils/tests/test_synthesize_extract_declarations.py index b690ee96e0..40f403dfbe 100644 --- a/cumulusci/tasks/bulkdata/extract_dataset_utils/tests/test_synthesize_extract_declarations.py +++ b/cumulusci/tasks/bulkdata/extract_dataset_utils/tests/test_synthesize_extract_declarations.py @@ -518,21 +518,26 @@ def _fake_get_org_schema( object_counts: T.Dict[str, int], **kwargs, ): - with mock.patch( - "cumulusci.salesforce_api.org_schema.count_sobjects", - lambda *args: ( - object_counts, - [], - [], + with ( + mock.patch( + "cumulusci.salesforce_api.org_schema.count_sobjects", + lambda *args: ( + object_counts, + [], + [], + ), + ), + mock.patch( + "cumulusci.salesforce_api.org_schema.ZippableTempDb", FakeZippableTempDb + ), + mock.patch( + "cumulusci.salesforce_api.org_schema.deep_describe", + return_value=( + (desc, "Sat, 1 Jan 2000 00:00:01 GMT") for desc in org_describes + ), ), - ), mock.patch( - "cumulusci.salesforce_api.org_schema.ZippableTempDb", FakeZippableTempDb - ), mock.patch( - "cumulusci.salesforce_api.org_schema.deep_describe", - return_value=((desc, "Sat, 1 Jan 2000 00:00:01 GMT") for desc in org_describes), - ), get_org_schema( - FakeSF(), org_config, **kwargs - ) as schema: + get_org_schema(FakeSF(), org_config, **kwargs) as schema, + ): yield schema diff --git a/cumulusci/tasks/bulkdata/generate_mapping_utils/dependency_map.py b/cumulusci/tasks/bulkdata/generate_mapping_utils/dependency_map.py index ebd5f6d2d4..23ecb84de0 100644 --- a/cumulusci/tasks/bulkdata/generate_mapping_utils/dependency_map.py +++ b/cumulusci/tasks/bulkdata/generate_mapping_utils/dependency_map.py @@ -43,9 +43,9 @@ def _map_references( for dep in intertable_dependencies: table_deps = self.dependencies[dep.table_name_from] table_deps.add(dep) - self.reference_fields[ - (dep.table_name_from, dep.field_name) - ] = dep.table_names_to + self.reference_fields[(dep.table_name_from, dep.field_name)] = ( + dep.table_names_to + ) def target_table_for( self, tablename: str, fieldname: str diff --git a/cumulusci/tasks/bulkdata/generate_mapping_utils/tests/test_generate_extract_mapping_from_declarations.py b/cumulusci/tasks/bulkdata/generate_mapping_utils/tests/test_generate_extract_mapping_from_declarations.py index f51f94db39..c465d6722a 100644 --- a/cumulusci/tasks/bulkdata/generate_mapping_utils/tests/test_generate_extract_mapping_from_declarations.py +++ b/cumulusci/tasks/bulkdata/generate_mapping_utils/tests/test_generate_extract_mapping_from_declarations.py @@ -34,7 +34,7 @@ def test_simple_generate_mapping_from_declarations(self, org_config): "api": "smart", "sf_object": "Account", "fields": ["Name", "Description"], - "soql_filter": "Name != 'Sample Account for " "Entitlements'", + "soql_filter": "Name != 'Sample Account for Entitlements'", } } diff --git a/cumulusci/tasks/bulkdata/load.py b/cumulusci/tasks/bulkdata/load.py index 2bb148869d..2f378ddfad 100644 --- a/cumulusci/tasks/bulkdata/load.py +++ b/cumulusci/tasks/bulkdata/load.py @@ -750,9 +750,9 @@ def _initialize_id_table(self, should_reset_table): Column("id", Unicode(255), primary_key=True), Column("sf_id", Unicode(18)), ) - if id_table.exists(): - id_table.drop() - id_table.create() + if self.inspector.has_table(self.ID_TABLE_NAME): + id_table.drop(self.metadata.bind) + id_table.create(self.metadata.bind) def _sqlite_load(self): """Read a SQLite script and initialize the in-memory database.""" diff --git a/cumulusci/tasks/bulkdata/mapping_parser.py b/cumulusci/tasks/bulkdata/mapping_parser.py index 63ed9c48f1..98b2041936 100644 --- a/cumulusci/tasks/bulkdata/mapping_parser.py +++ b/cumulusci/tasks/bulkdata/mapping_parser.py @@ -47,6 +47,7 @@ def has_errors(self) -> bool: class MappingLookup(CCIDictModel): "Lookup relationship between two tables." + table: Union[str, List[str]] # Support for polymorphic lookups key_field: Optional[str] = None value_field: Optional[str] = None @@ -102,6 +103,7 @@ class BulkMode(StrEnum): class MappingStep(CCIDictModel): "Step in a load or extract process" + sf_object: str table: Optional[str] = None fields_: Dict[str, str] = Field({}, alias="fields") @@ -113,9 +115,9 @@ class MappingStep(CCIDictModel): batch_size: int = None oid_as_pk: bool = False # this one should be discussed and probably deprecated record_type: Optional[str] = None # should be discussed and probably deprecated - bulk_mode: Optional[ - Literal["Serial", "Parallel"] - ] = None # default should come from task options + bulk_mode: Optional[Literal["Serial", "Parallel"]] = ( + None # default should come from task options + ) anchor_date: Optional[Union[str, date]] = None soql_filter: Optional[str] = None # soql_filter property select_options: Optional[SelectOptions] = Field( @@ -138,9 +140,9 @@ def split_update_key(cls, val): if isinstance(val, str): return tuple(v.strip() for v in val.split(",")) else: - assert isinstance( - val, (str, list, tuple) - ), "`update_key` should be a field name or list of field names." + assert isinstance(val, (str, list, tuple)), ( + "`update_key` should be a field name or list of field names." + ) assert False, "Should be unreachable" # pragma: no cover @root_validator @@ -336,9 +338,9 @@ def validate_update_key_and_upsert(cls, v): if action == DataOperationType.UPSERT: assert update_key, "'update_key' must always be supplied for upsert." - assert ( - len(update_key) == 1 - ), "simple upserts can only support one field at a time." + assert len(update_key) == 1, ( + "simple upserts can only support one field at a time." + ) elif action in (DataOperationType.ETL_UPSERT, DataOperationType.SMART_UPSERT): assert update_key, "'update_key' must always be supplied for upsert." else: @@ -346,9 +348,9 @@ def validate_update_key_and_upsert(cls, v): if update_key: for key in update_key: - assert key.lower() in ( - f.lower() for f in v["fields_"] - ), f"`update_key`: {key} not found in `fields``" + assert key.lower() in (f.lower() for f in v["fields_"]), ( + f"`update_key`: {key} not found in `fields``" + ) return v @@ -677,6 +679,7 @@ def dict(self, by_alias=True, exclude_defaults=True, **kwargs): class MappingSteps(CCIDictModel): "Mapping of named steps" + __root__: Dict[str, MappingStep] @root_validator(pre=False) @@ -684,9 +687,9 @@ class MappingSteps(CCIDictModel): def validate_and_inject_mapping(cls, values): if values: oids = ["Id" in s.fields_ for s in values["__root__"].values()] - assert all(oids) or not any( - oids - ), "Id must be mapped in all steps or in no steps." + assert all(oids) or not any(oids), ( + "Id must be mapped in all steps or in no steps." + ) return values diff --git a/cumulusci/tasks/bulkdata/query_transformers.py b/cumulusci/tasks/bulkdata/query_transformers.py index 3f632c694e..9827d9795a 100644 --- a/cumulusci/tasks/bulkdata/query_transformers.py +++ b/cumulusci/tasks/bulkdata/query_transformers.py @@ -243,7 +243,6 @@ def outerjoins_to_add(self): ] except KeyError as f: - raise BulkDataException( "A record type mapping table was not found in your dataset. " f"Was it generated by extract_data? {e}", diff --git a/cumulusci/tasks/bulkdata/snowfakery.py b/cumulusci/tasks/bulkdata/snowfakery.py index 54645ac290..818d189488 100644 --- a/cumulusci/tasks/bulkdata/snowfakery.py +++ b/cumulusci/tasks/bulkdata/snowfakery.py @@ -61,7 +61,6 @@ class Snowfakery(BaseSalesforceApiTask): - task_docs = """ Do a data load with Snowfakery. diff --git a/cumulusci/tasks/bulkdata/tests/test_extract.py b/cumulusci/tasks/bulkdata/tests/test_extract.py index 996584a2a5..e6ccdcaa8d 100644 --- a/cumulusci/tasks/bulkdata/tests/test_extract.py +++ b/cumulusci/tasks/bulkdata/tests/test_extract.py @@ -45,12 +45,15 @@ def _job_state_from_batches(self, job_id): def get_results(self): return extracted_records[self.sobject] - with mock.patch( - "cumulusci.tasks.bulkdata.step.BulkApiQueryOperation.get_results", - get_results, - ), mock.patch( - "cumulusci.tasks.bulkdata.step.BulkJobMixin._job_state_from_batches", - _job_state_from_batches, + with ( + mock.patch( + "cumulusci.tasks.bulkdata.step.BulkApiQueryOperation.get_results", + get_results, + ), + mock.patch( + "cumulusci.tasks.bulkdata.step.BulkJobMixin._job_state_from_batches", + _job_state_from_batches, + ), ): yield @@ -81,7 +84,6 @@ def query(self): class TestExtractData: - mapping_file_v1 = "mapping_v1.yml" mapping_file_v2 = "mapping_v2.yml" mapping_file_poly = "mapping_poly.yml" @@ -187,7 +189,6 @@ def test_run__person_accounts_enabled(self, query_op_mock): task() with create_engine(task.options["database_url"]).connect() as conn: - household = next(conn.execute("select * from households")) assert household.sf_id == "1" assert household.IsPersonAccount == "false" @@ -238,12 +239,12 @@ def test_run__sql(self, query_op_mock): task() assert os.path.exists("testdata.sql") - assert ce_mock.mock_calls[0][1][0].endswith( - "temp_db.db" - ), ce_mock.mock_calls[0][1][0] - assert ce_mock.mock_calls[0][1][0].startswith( - "sqlite:///" - ), ce_mock.mock_calls[0][1][0] + assert ce_mock.mock_calls[0][1][0].endswith("temp_db.db"), ( + ce_mock.mock_calls[0][1][0] + ) + assert ce_mock.mock_calls[0][1][0].startswith("sqlite:///"), ( + ce_mock.mock_calls[0][1][0] + ) @responses.activate @mock.patch("cumulusci.tasks.bulkdata.extract.get_query_operation") @@ -757,9 +758,7 @@ def test_convert_lookups_to_id(self): } task.session.query.return_value.filter.return_value.count.return_value = 0 - task.session.query.return_value.filter.return_value.update.return_value.rowcount = ( - 0 - ) + task.session.query.return_value.filter.return_value.update.return_value.rowcount = 0 task._convert_lookups_to_id( MappingStep( sf_object="Opportunity", @@ -1199,8 +1198,9 @@ def test_import_results__autopk(self, create_task_fixture): ] ], } - with mock_extract_jobs(task, extracted_records), mock_salesforce_client( - task + with ( + mock_extract_jobs(task, extracted_records), + mock_salesforce_client(task), ): task() with create_engine(task.options["database_url"]).connect() as conn: @@ -1303,9 +1303,9 @@ def test_run_soql_filter_no_record_type(self): ) soql = task._soql_for_mapping(mapping) - assert ( - "WHERE Name = 'John Doe'" in soql - ), "filter should be applied just on name" - assert ( - "DeveloperName" not in soql - ), "DeveloperName should not appear in the soql query as it is missing in mapping" + assert "WHERE Name = 'John Doe'" in soql, ( + "filter should be applied just on name" + ) + assert "DeveloperName" not in soql, ( + "DeveloperName should not appear in the soql query as it is missing in mapping" + ) diff --git a/cumulusci/tasks/bulkdata/tests/test_generate_from_snowfakery_task.py b/cumulusci/tasks/bulkdata/tests/test_generate_from_snowfakery_task.py index 87845ebc8b..54d4d5d59d 100644 --- a/cumulusci/tasks/bulkdata/tests/test_generate_from_snowfakery_task.py +++ b/cumulusci/tasks/bulkdata/tests/test_generate_from_snowfakery_task.py @@ -376,7 +376,9 @@ def test_generate_continuation_file(self): ) task() continuation_file = yaml.safe_load(open(temp_continuation_file)) - assert continuation_file # internals of this file are not important to CumulusCI + assert ( + continuation_file + ) # internals of this file are not important to CumulusCI def _get_mapping_file(self, **options): with temporary_file_path("mapping.yml") as temp_mapping: diff --git a/cumulusci/tasks/bulkdata/tests/test_load.py b/cumulusci/tasks/bulkdata/tests/test_load.py index f413cf4ed7..1ffbd8a248 100644 --- a/cumulusci/tasks/bulkdata/tests/test_load.py +++ b/cumulusci/tasks/bulkdata/tests/test_load.py @@ -266,11 +266,14 @@ def test__perform_rollback(self): task.metadata = mock.Mock() task.metadata.sorted_tables = [table_insert, table_upsert] - with mock.patch.object( - CreateRollback, "_perform_rollback" - ) as mock_insert_rollback, mock.patch.object( - UpdateRollback, "_perform_rollback" - ) as mock_upsert_rollback: + with ( + mock.patch.object( + CreateRollback, "_perform_rollback" + ) as mock_insert_rollback, + mock.patch.object( + UpdateRollback, "_perform_rollback" + ) as mock_upsert_rollback, + ): Rollback._perform_rollback(task) mock_insert_rollback.assert_called_once_with(task, table_insert) @@ -862,9 +865,10 @@ def test_process_lookup_fields_polymorphic(self): "Who.Contact.LastName", "Who.Lead.LastName", } - with mock.patch( - "cumulusci.tasks.bulkdata.load.validate_and_inject_mapping" - ), mock.patch.object(task, "sf", create=True): + with ( + mock.patch("cumulusci.tasks.bulkdata.load.validate_and_inject_mapping"), + mock.patch.object(task, "sf", create=True), + ): task._init_mapping() with task._init_db(): task._old_format = mock.Mock(return_value=False) @@ -911,9 +915,10 @@ def test_process_lookup_fields_non_polymorphic(self): "Account.Name", "Account.AccountNumber", } - with mock.patch( - "cumulusci.tasks.bulkdata.load.validate_and_inject_mapping" - ), mock.patch.object(task, "sf", create=True): + with ( + mock.patch("cumulusci.tasks.bulkdata.load.validate_and_inject_mapping"), + mock.patch.object(task, "sf", create=True), + ): task._init_mapping() with task._init_db(): task._old_format = mock.Mock(return_value=False) @@ -981,15 +986,17 @@ def test_get_statics_record_type_not_matched(self): task.sf = mock.Mock() task.sf.query.return_value = {"records": []} with pytest.raises(BulkDataException) as e: - task._get_statics( - MappingStep( - sf_object="Account", - action="insert", - fields={"Id": "sf_id", "Name": "Name"}, - static={"Industry": "Technology"}, - record_type="Organization", - ) - ), + ( + task._get_statics( + MappingStep( + sf_object="Account", + action="insert", + fields={"Id": "sf_id", "Name": "Name"}, + static={"Industry": "Technology"}, + record_type="Organization", + ) + ), + ) assert "RecordType" in str(e.value) def test_query_db__joins_self_lookups(self): @@ -1290,7 +1297,7 @@ def test_initialize_id_table__already_exists(self): id_table.create() task._initialize_id_table(True) new_id_table = task.metadata.tables["cumulusci_id_table"] - assert not (new_id_table is id_table) + assert new_id_table is not id_table def test_initialize_id_table__already_exists_and_should_not_reset_table(self): task = _make_task( @@ -1469,9 +1476,10 @@ def test_process_job_results__exception_failure(self): mapping = MappingStep(sf_object="Account", action=DataOperationType.UPDATE) - with mock.patch( - "cumulusci.tasks.bulkdata.load.sql_bulk_insert_from_records" - ), pytest.raises(BulkDataException) as e: + with ( + mock.patch("cumulusci.tasks.bulkdata.load.sql_bulk_insert_from_records"), + pytest.raises(BulkDataException) as e, + ): task._process_job_results(mapping, step, local_ids) assert "Error on record with id" in str(e.value) @@ -1880,11 +1888,15 @@ def test_generate_results_id_map__exception_failure_with_rollback(self): ] ) - with pytest.raises(BulkDataException) as e, mock.patch( - "cumulusci.tasks.bulkdata.load.Rollback._perform_rollback" - ) as mock_rollback, mock.patch( - "cumulusci.tasks.bulkdata.load.sql_bulk_insert_from_records" - ) as mock_insert_records: + with ( + pytest.raises(BulkDataException) as e, + mock.patch( + "cumulusci.tasks.bulkdata.load.Rollback._perform_rollback" + ) as mock_rollback, + mock.patch( + "cumulusci.tasks.bulkdata.load.sql_bulk_insert_from_records" + ) as mock_insert_records, + ): task._generate_results_id_map( step, ["001000000000009", "001000000000010", "001000000000011"] ) @@ -2880,14 +2892,16 @@ def _job_state_from_batches(self, job_id): MEGABYTE = 2**20 # FIXME: more anlysis about the number below - with mock.patch( - "cumulusci.tasks.bulkdata.step.BulkJobMixin._job_state_from_batches", - _job_state_from_batches, - ), mock.patch( - "cumulusci.tasks.bulkdata.step.BulkApiDmlOperation.get_results", - get_results, - ), assert_max_memory_usage( - 15 * MEGABYTE + with ( + mock.patch( + "cumulusci.tasks.bulkdata.step.BulkJobMixin._job_state_from_batches", + _job_state_from_batches, + ), + mock.patch( + "cumulusci.tasks.bulkdata.step.BulkApiDmlOperation.get_results", + get_results, + ), + assert_max_memory_usage(15 * MEGABYTE), ): task() @@ -3057,9 +3071,10 @@ def test_smart_lookup__mixed_sf_ids_and_local_refs(self): }, ) - with mock.patch( - "cumulusci.tasks.bulkdata.load.validate_and_inject_mapping" - ), mock.patch.object(task, "sf", create=True): + with ( + mock.patch("cumulusci.tasks.bulkdata.load.validate_and_inject_mapping"), + mock.patch.object(task, "sf", create=True), + ): task._init_mapping() with task._init_db(): @@ -3193,9 +3208,10 @@ def _validate_query_for_mapping_step( } }, ) - with mock.patch( - "cumulusci.tasks.bulkdata.load.validate_and_inject_mapping" - ), mock.patch.object(task, "sf", create=True): + with ( + mock.patch("cumulusci.tasks.bulkdata.load.validate_and_inject_mapping"), + mock.patch.object(task, "sf", create=True), + ): task._init_mapping() with task._init_db(): task._old_format = mock.Mock(return_value=old_format) diff --git a/cumulusci/tasks/bulkdata/tests/test_select_utils.py b/cumulusci/tasks/bulkdata/tests/test_select_utils.py index 3c9addd32d..efa9502902 100644 --- a/cumulusci/tasks/bulkdata/tests/test_select_utils.py +++ b/cumulusci/tasks/bulkdata/tests/test_select_utils.py @@ -397,9 +397,9 @@ def test_calculate_levenshtein_distance_basic(): expected_distance = (1 / 5 * 1.0 + 1 / 5 * 1.0) / 2 # Averaged over two fields result = calculate_levenshtein_distance(record1, record2, weights) - assert result == pytest.approx( - expected_distance - ), "Basic distance calculation failed." + assert result == pytest.approx(expected_distance), ( + "Basic distance calculation failed." + ) # Empty fields record1 = ["hello", ""] @@ -411,9 +411,9 @@ def test_calculate_levenshtein_distance_basic(): expected_distance = (1 / 5 * 1.0 + 0 * 1.0) / 2 # Averaged over two fields result = calculate_levenshtein_distance(record1, record2, weights) - assert result == pytest.approx( - expected_distance - ), "Basic distance calculation with empty fields failed." + assert result == pytest.approx(expected_distance), ( + "Basic distance calculation with empty fields failed." + ) # Partial empty fields record1 = ["hello", "world"] @@ -427,9 +427,9 @@ def test_calculate_levenshtein_distance_basic(): ) / 2 # Averaged over two fields result = calculate_levenshtein_distance(record1, record2, weights) - assert result == pytest.approx( - expected_distance - ), "Basic distance calculation with partial empty fields failed." + assert result == pytest.approx(expected_distance), ( + "Basic distance calculation with partial empty fields failed." + ) def test_calculate_levenshtein_distance_weighted(): @@ -443,9 +443,9 @@ def test_calculate_levenshtein_distance_weighted(): ) / 2.5 # Weighted average over two fields result = calculate_levenshtein_distance(record1, record2, weights) - assert result == pytest.approx( - expected_distance - ), "Weighted distance calculation failed." + assert result == pytest.approx(expected_distance), ( + "Weighted distance calculation failed." + ) def test_calculate_levenshtein_distance_records_length_doesnt_match(): @@ -600,18 +600,18 @@ def test_vectorize_records_mixed_numerical_boolean_categorical(): # Check the shape of the output vectors assert final_db_vectors.shape[0] == len(db_records), "DB vectors row count mismatch" - assert final_query_vectors.shape[0] == len( - query_records - ), "Query vectors row count mismatch" + assert final_query_vectors.shape[0] == len(query_records), ( + "Query vectors row count mismatch" + ) # Expected dimensions: numerical (1) + categorical hashed features (4) expected_feature_count = 2 + hash_features - assert ( - final_db_vectors.shape[1] == expected_feature_count - ), "DB vectors column count mismatch" - assert ( - final_query_vectors.shape[1] == expected_feature_count - ), "Query vectors column count mismatch" + assert final_db_vectors.shape[1] == expected_feature_count, ( + "DB vectors column count mismatch" + ) + assert final_query_vectors.shape[1] == expected_feature_count, ( + "Query vectors column count mismatch" + ) def _build_large_annoy_fixture(): diff --git a/cumulusci/tasks/bulkdata/tests/test_snowfakery.py b/cumulusci/tasks/bulkdata/tests/test_snowfakery.py index 22789aac9b..9f4b6d77d1 100644 --- a/cumulusci/tasks/bulkdata/tests/test_snowfakery.py +++ b/cumulusci/tasks/bulkdata/tests/test_snowfakery.py @@ -95,7 +95,6 @@ def __call__(self, *args, **kwargs): in a normal mock_values structure.""" with self.lock: # the code below looks thread-safe but better safe than sorry - # tasks usually aren't called twice after being instantiated # that would usually be a bug. assert self not in self.mock_calls @@ -153,11 +152,14 @@ def mock_load_data( ): fake_load_data = FakeLoadData - with mock.patch( - "cumulusci.tasks.bulkdata.generate_and_load_data.LoadData", fake_load_data - ), mock.patch( - "cumulusci.tasks.bulkdata.snowfakery_utils.queue_manager.LoadData", - fake_load_data, + with ( + mock.patch( + "cumulusci.tasks.bulkdata.generate_and_load_data.LoadData", fake_load_data + ), + mock.patch( + "cumulusci.tasks.bulkdata.snowfakery_utils.queue_manager.LoadData", + fake_load_data, + ), ): fake_load_data.reset() @@ -187,12 +189,15 @@ def __call__(self, target, args, daemon): process_manager = FakeProcessManager() - with mock.patch( - "cumulusci.utils.parallel.task_worker_queues.parallel_worker_queue.WorkerQueue.Thread", - process_manager, - ), mock.patch( - "cumulusci.utils.parallel.task_worker_queues.parallel_worker_queue.WorkerQueue.Process", - process_manager, + with ( + mock.patch( + "cumulusci.utils.parallel.task_worker_queues.parallel_worker_queue.WorkerQueue.Thread", + process_manager, + ), + mock.patch( + "cumulusci.utils.parallel.task_worker_queues.parallel_worker_queue.WorkerQueue.Process", + process_manager, + ), ): yield process_manager @@ -394,9 +399,9 @@ def get_record_counts_from_snowfakery_results( channeled_outboxes = tuple(results.working_dir.glob("*/data_load_outbox/*")) regular_outboxes = tuple(results.working_dir.glob("data_load_outbox/*")) - assert bool(regular_outboxes) ^ bool( - channeled_outboxes - ), f"One of regular_outboxes or channeled_outboxes should be available: {channeled_outboxes}, {regular_outboxes}" + assert bool(regular_outboxes) ^ bool(channeled_outboxes), ( + f"One of regular_outboxes or channeled_outboxes should be available: {channeled_outboxes}, {regular_outboxes}" + ) outboxes = tuple(channeled_outboxes) + tuple(regular_outboxes) for subdir in outboxes: record_counts = SnowfakeryWorkingDirectory(subdir).get_record_counts() @@ -559,9 +564,9 @@ def test_run_until_records_in_org__none_needed( ) task() assert len(mock_load_data.mock_calls) == 0, mock_load_data.mock_calls - assert ( - len(threads_instead_of_processes.mock_calls) == 0 - ), threads_instead_of_processes.mock_calls + assert len(threads_instead_of_processes.mock_calls) == 0, ( + threads_instead_of_processes.mock_calls + ) @pytest.mark.vcr() @mock.patch("cumulusci.tasks.bulkdata.snowfakery.MIN_PORTION_SIZE", 5) @@ -600,9 +605,9 @@ def test_run_until_records_in_org__multiple_needed( task() assert len(mock_load_data.mock_calls) == 2, mock_load_data.mock_calls - assert ( - len(threads_instead_of_processes.mock_calls) == 1 - ), threads_instead_of_processes.mock_calls + assert len(threads_instead_of_processes.mock_calls) == 1, ( + threads_instead_of_processes.mock_calls + ) def test_inaccessible_generator_yaml(self, snowfakery): with pytest.raises(exc.TaskOptionsError, match="recipe"): @@ -622,9 +627,12 @@ def test_snowfakery_debug_mode_and_cpu_count(self, snowfakery, mock_load_data): @mock.patch("cumulusci.tasks.bulkdata.snowfakery.MIN_PORTION_SIZE", 3) def test_record_count(self, snowfakery, mock_load_data): task = snowfakery(recipe="datasets/recipe.yml", run_until_recipe_repeated="4") - with mock.patch.object(task, "logger") as logger, mock.patch.object( - task.project_config, "keychain", DummyKeychain() - ) as keychain: + with ( + mock.patch.object(task, "logger") as logger, + mock.patch.object( + task.project_config, "keychain", DummyKeychain() + ) as keychain, + ): def get_org(username): return DummyOrgConfig( @@ -904,9 +912,12 @@ def test_channels_cli_options_conflict(self, create_task): "recipe_options": {"xyzzy": "Nothing happens", "some_number": 37}, }, ) - with pytest.raises(exc.TaskOptionsError) as e, mock.patch.object( - task.project_config, "keychain", DummyKeychain() - ) as keychain: + with ( + pytest.raises(exc.TaskOptionsError) as e, + mock.patch.object( + task.project_config, "keychain", DummyKeychain() + ) as keychain, + ): def get_org(username): return DummyOrgConfig( @@ -933,9 +944,12 @@ def test_explicit_channel_declarations(self, mock_load_data, create_task): / "snowfakery/simple_snowfakery_channels.load.yml", }, ) - with pytest.warns(UserWarning), mock.patch.object( - task.project_config, "keychain", DummyKeychain() - ) as keychain: + with ( + pytest.warns(UserWarning), + mock.patch.object( + task.project_config, "keychain", DummyKeychain() + ) as keychain, + ): def get_org(username): return DummyOrgConfig( @@ -1159,9 +1173,12 @@ def test_too_many_channel_declarations(self, mock_load_data, create_task): / "snowfakery/simple_snowfakery_channels_2.load.yml", }, ) - with pytest.raises(exc.TaskOptionsError), mock.patch.object( - task.project_config, "keychain", DummyKeychain() - ) as keychain: + with ( + pytest.raises(exc.TaskOptionsError), + mock.patch.object( + task.project_config, "keychain", DummyKeychain() + ) as keychain, + ): def get_org(username): return DummyOrgConfig( diff --git a/cumulusci/tasks/bulkdata/tests/test_step.py b/cumulusci/tasks/bulkdata/tests/test_step.py index 25a8362a54..4bbc4e6880 100644 --- a/cumulusci/tasks/bulkdata/tests/test_step.py +++ b/cumulusci/tasks/bulkdata/tests/test_step.py @@ -139,9 +139,9 @@ def test_parse_job_state(self): " 200" " " "" - ) == DataOperationJobResult( - DataOperationStatus.ROW_FAILURE, [], 0, 400 - ), "Multiple batches in single job" + ) == DataOperationJobResult(DataOperationStatus.ROW_FAILURE, [], 0, 400), ( + "Multiple batches in single job" + ) assert mixin._parse_job_state( '' @@ -150,9 +150,9 @@ def test_parse_job_state(self): " 200" " " "" - ) == DataOperationJobResult( - DataOperationStatus.ROW_FAILURE, [], 0, 200 - ), "Single batch" + ) == DataOperationJobResult(DataOperationStatus.ROW_FAILURE, [], 0, 200), ( + "Single batch" + ) assert mixin._parse_job_state( '' @@ -167,9 +167,9 @@ def test_parse_job_state(self): " 10" " " "" - ) == DataOperationJobResult( - DataOperationStatus.ROW_FAILURE, [], 20, 400 - ), "Multiple batches in single job" + ) == DataOperationJobResult(DataOperationStatus.ROW_FAILURE, [], 20, 400), ( + "Multiple batches in single job" + ) assert mixin._parse_job_state( '' @@ -177,9 +177,9 @@ def test_parse_job_state(self): " 200" " 10" "" - ) == DataOperationJobResult( - DataOperationStatus.ROW_FAILURE, [], 10, 200 - ), "Single batch" + ) == DataOperationJobResult(DataOperationStatus.ROW_FAILURE, [], 10, 200), ( + "Single batch" + ) @mock.patch("time.sleep") def test_wait_for_job(self, sleep_patch): @@ -527,8 +527,12 @@ def test_get_prev_record_values(self): step.bulk.get_all_results_for_query_batch.return_value = results records = iter([["Test1"], ["Test2"], ["Test3"]]) - with mock.patch("json.load", side_effect=lambda result: result), mock.patch( - "salesforce_bulk.util.IteratorBytesIO", side_effect=lambda result: result + with ( + mock.patch("json.load", side_effect=lambda result: result), + mock.patch( + "salesforce_bulk.util.IteratorBytesIO", + side_effect=lambda result: result, + ), ): prev_record_values, relevant_fields = step.get_prev_record_values(records) diff --git a/cumulusci/tasks/bulkdata/tests/test_updates.py b/cumulusci/tasks/bulkdata/tests/test_updates.py index da469b0dca..c5be4cd81f 100644 --- a/cumulusci/tasks/bulkdata/tests/test_updates.py +++ b/cumulusci/tasks/bulkdata/tests/test_updates.py @@ -40,12 +40,15 @@ def activate(self, func): @wraps(func) def wrapper(*args, **kwds): self.mock_bulk_API_responses_context = MockBulkAPIResponsesContext() - with mock.patch( - "cumulusci.tasks.bulkdata.update_data.get_query_operation", - self.mock_bulk_API_responses_context.get_query_operation, - ), mock.patch( - "cumulusci.tasks.bulkdata.update_data.get_dml_operation", - self.mock_bulk_API_responses_context.get_dml_operation, + with ( + mock.patch( + "cumulusci.tasks.bulkdata.update_data.get_query_operation", + self.mock_bulk_API_responses_context.get_query_operation, + ), + mock.patch( + "cumulusci.tasks.bulkdata.update_data.get_dml_operation", + self.mock_bulk_API_responses_context.get_dml_operation, + ), ): try: ret = func(*args, **kwds) @@ -492,7 +495,6 @@ def test_update_row_errors_exception_catching(self, create_task): class TestUpdatesIntegrationTests: - # VCR doesn't match because of randomized data @pytest.mark.vcr() def test_updates_task(self, create_task, ensure_accounts): diff --git a/cumulusci/tasks/bulkdata/tests/test_upsert.py b/cumulusci/tasks/bulkdata/tests/test_upsert.py index aa23c50fcb..e6da87212e 100644 --- a/cumulusci/tasks/bulkdata/tests/test_upsert.py +++ b/cumulusci/tasks/bulkdata/tests/test_upsert.py @@ -236,9 +236,9 @@ def test_upsert_rest__faked( relevant_debug_statement = look_for_operation_creation_debug_statement( task.logger.debug.mock_calls ) - assert relevant_debug_statement == format( - DataApi.REST - ), relevant_debug_statement + assert relevant_debug_statement == format(DataApi.REST), ( + relevant_debug_statement + ) def _mock_bulk(self, domain): responses.add( @@ -395,40 +395,43 @@ def test_upsert__fake_bulk(self, create_task, cumulusci_test_repo_root, org_conf with mock.patch.object(task.logger, "debug"): ret = task() - assert ret == { - "step_results": { - "Insert Accounts": { - "sobject": "Account", - "record_type": None, - "status": DataOperationStatus.SUCCESS, - "job_errors": [], - "records_processed": 0, - "total_row_errors": 0, - }, - "Upsert Contacts": { - "sobject": "Contact", - "record_type": None, - "status": DataOperationStatus.SUCCESS, - "job_errors": [], - "records_processed": 0, # change here and above to 4 to match data - "total_row_errors": 0, - }, - "Insert Opportunities": { - "sobject": "Opportunity", - "record_type": None, - "status": DataOperationStatus.SUCCESS, - "job_errors": [], - "records_processed": 0, - "total_row_errors": 0, - }, + assert ( + ret + == { + "step_results": { + "Insert Accounts": { + "sobject": "Account", + "record_type": None, + "status": DataOperationStatus.SUCCESS, + "job_errors": [], + "records_processed": 0, + "total_row_errors": 0, + }, + "Upsert Contacts": { + "sobject": "Contact", + "record_type": None, + "status": DataOperationStatus.SUCCESS, + "job_errors": [], + "records_processed": 0, # change here and above to 4 to match data + "total_row_errors": 0, + }, + "Insert Opportunities": { + "sobject": "Opportunity", + "record_type": None, + "status": DataOperationStatus.SUCCESS, + "job_errors": [], + "records_processed": 0, + "total_row_errors": 0, + }, + } } - }, ret + ), ret relevant_debug_statement = look_for_operation_creation_debug_statement( task.logger.debug.mock_calls ) - assert relevant_debug_statement in format( - DataApi.BULK - ), relevant_debug_statement + assert relevant_debug_statement in format(DataApi.BULK), ( + relevant_debug_statement + ) def _test_two_upserts_and_check_results__complex( self, api, create_task, cumulusci_test_repo_root, sf diff --git a/cumulusci/tasks/command.py b/cumulusci/tasks/command.py index 1935fc3e6f..ad227b28c3 100644 --- a/cumulusci/tasks/command.py +++ b/cumulusci/tasks/command.py @@ -1,4 +1,4 @@ -""" Tasks for running a command in a subprocess +"""Tasks for running a command in a subprocess Command - run a command with optional environment variables SalesforceCommand - run a command with credentials passed diff --git a/cumulusci/tasks/create_package_version.py b/cumulusci/tasks/create_package_version.py index 9933b3e49e..ff15b2a905 100644 --- a/cumulusci/tasks/create_package_version.py +++ b/cumulusci/tasks/create_package_version.py @@ -271,7 +271,7 @@ def _run_task(self): self.options.get("install_key"), ) res = self.tooling.query( - "SELECT Dependencies FROM SubscriberPackageVersion " f"WHERE {where_clause}" + f"SELECT Dependencies FROM SubscriberPackageVersion WHERE {where_clause}" ) self.return_values["dependencies"] = self._prepare_cci_dependencies( res["records"][0]["Dependencies"] @@ -323,7 +323,7 @@ def _get_or_create_package(self, package_config: PackageConfig): if existing_package["ContainerOptions"] != package_config.package_type: raise PackageUploadFailure( f"Duplicate Package: {existing_package['ContainerOptions']} package with id " - f"{ existing_package['Id']} has the same name ({package_config.package_name}) " + f"{existing_package['Id']} has the same name ({package_config.package_name}) " "for this namespace but has a different package type" ) package_id = existing_package["Id"] @@ -391,9 +391,9 @@ def _create_version_request( } if package_config.post_install_script: - package_descriptor[ - "postInstallScript" - ] = package_config.post_install_script + package_descriptor["postInstallScript"] = ( + package_config.post_install_script + ) if package_config.uninstall_script: package_descriptor["uninstallScript"] = package_config.uninstall_script diff --git a/cumulusci/tasks/datadictionary.py b/cumulusci/tasks/datadictionary.py index 04418512ce..9eec3a61e1 100644 --- a/cumulusci/tasks/datadictionary.py +++ b/cumulusci/tasks/datadictionary.py @@ -133,14 +133,14 @@ def _init_options(self, kwargs): super()._init_options(kwargs) if self.options.get("object_path") is None: - self.options[ - "object_path" - ] = f"{self.project_config.project__name} Objects.csv" + self.options["object_path"] = ( + f"{self.project_config.project__name} Objects.csv" + ) if self.options.get("field_path") is None: - self.options[ - "field_path" - ] = f"{self.project_config.project__name} Fields.csv" + self.options["field_path"] = ( + f"{self.project_config.project__name} Fields.csv" + ) include_dependencies = self.options.get("include_dependencies") self.options["include_dependencies"] = process_bool_arg( @@ -679,13 +679,13 @@ def _write_field_results(self, file_handle): # Locate the last versions where the valid values and the help text changed. valid_values_version = None - for (index, version) in enumerate(versions[1:]): + for index, version in enumerate(versions[1:]): if version.valid_values != last_version.valid_values: valid_values_version = versions[index] break help_text_version = None - for (index, version) in enumerate(versions[1:]): + for index, version in enumerate(versions[1:]): if version.help_text != last_version.help_text: help_text_version = versions[index] break diff --git a/cumulusci/tasks/github/merge.py b/cumulusci/tasks/github/merge.py index 1cc32887fa..569bbe578f 100644 --- a/cumulusci/tasks/github/merge.py +++ b/cumulusci/tasks/github/merge.py @@ -45,13 +45,13 @@ def _init_options(self, kwargs): if "commit" not in self.options: self.options["commit"] = self.project_config.repo_commit if "branch_prefix" not in self.options: - self.options[ - "branch_prefix" - ] = self.project_config.project__git__prefix_feature + self.options["branch_prefix"] = ( + self.project_config.project__git__prefix_feature + ) if "source_branch" not in self.options: - self.options[ - "source_branch" - ] = self.project_config.project__git__default_branch + self.options["source_branch"] = ( + self.project_config.project__git__default_branch + ) if "skip_future_releases" not in self.options: self.options["skip_future_releases"] = True else: diff --git a/cumulusci/tasks/github/release.py b/cumulusci/tasks/github/release.py index bda1703145..48ff28b2ea 100644 --- a/cumulusci/tasks/github/release.py +++ b/cumulusci/tasks/github/release.py @@ -13,7 +13,6 @@ class CreateRelease(BaseGithubTask): - task_options = { "version": { "description": "The managed package version number. Ex: 1.2", diff --git a/cumulusci/tasks/github/tests/test_merge.py b/cumulusci/tasks/github/tests/test_merge.py index 80743f7cb3..70c249e433 100644 --- a/cumulusci/tasks/github/tests/test_merge.py +++ b/cumulusci/tasks/github/tests/test_merge.py @@ -1019,7 +1019,7 @@ def test_is_release_branch(self): ] invalid_release_branches = [ f"{prefix}200_", - f"{prefix}_200" f"{prefix}230_", + f"{prefix}_200{prefix}230_", f"{prefix}230__child", f"{prefix}230__grand__child", f"{prefix}230a", diff --git a/cumulusci/tasks/github/util.py b/cumulusci/tasks/github/util.py index c838a20bd8..35830897b1 100644 --- a/cumulusci/tasks/github/util.py +++ b/cumulusci/tasks/github/util.py @@ -89,7 +89,7 @@ def _create_new_tree_item(self, item: dict) -> Optional[dict]: """ if not item["path"].startswith(self.repo_dir): # outside target dir in repo - keep in tree - self.logger.debug(f'Unchanged (outside target path): {item["path"]}') + self.logger.debug(f"Unchanged (outside target path): {item['path']}") return None local_path, content = self._find_and_read_item(item) @@ -105,7 +105,7 @@ def _create_new_tree_item(self, item: dict) -> Optional[dict]: new_item.pop("sha", None) new_item.update(self._get_content_or_sha(content, self.dry_run)) else: - self.logger.debug(f'Unchanged: {item["path"]}') + self.logger.debug(f"Unchanged: {item['path']}") return None return new_item @@ -124,7 +124,7 @@ def _add_new_files_to_tree(self, new_tree_list): if local_file_subpath not in new_tree_target_subpaths: repo_path = f"{self.repo_dir}/" if self.repo_dir else "" new_item = { - "path": f'{repo_path}{local_file_subpath.replace(os.sep, "/")}', + "path": f"{repo_path}{local_file_subpath.replace(os.sep, '/')}", "mode": "100644", # FIXME: This is wrong "type": "blob", } diff --git a/cumulusci/tasks/marketing_cloud/deploy.py b/cumulusci/tasks/marketing_cloud/deploy.py index 840fe942b4..63ef4e7bd8 100644 --- a/cumulusci/tasks/marketing_cloud/deploy.py +++ b/cumulusci/tasks/marketing_cloud/deploy.py @@ -301,9 +301,9 @@ def _poll_update_interval(self): def _process_completed_deploy(self, response_data: Dict): deploy_status = response_data["status"] - assert ( - deploy_status != IN_PROGRESS_STATUSES - ), "Deploy should be in a completed state before processing." + assert deploy_status != IN_PROGRESS_STATUSES, ( + "Deploy should be in a completed state before processing." + ) self.poll_complete = True if deploy_status in FINISHED_STATUSES: diff --git a/cumulusci/tasks/metadata/package.py b/cumulusci/tasks/metadata/package.py index d7689d8eb0..fce285806f 100644 --- a/cumulusci/tasks/metadata/package.py +++ b/cumulusci/tasks/metadata/package.py @@ -46,7 +46,6 @@ def process_common_components(response_messages: List, components: Dict): if not response_messages or not components: return components for message in response_messages: - message_list = message.firstChild.nextSibling.firstChild.nodeValue.split("'") if len(message_list) > 1: component_type = message_list[1] @@ -353,7 +352,6 @@ class ParserConfigurationError(Exception): class MetadataXmlElementParser(BaseMetadataParser): - namespaces = {"sf": "http://soap.sforce.com/2006/04/metadata"} def __init__( diff --git a/cumulusci/tasks/metadata/tests/test_package.py b/cumulusci/tasks/metadata/tests/test_package.py index 4ba10fba43..c489be8bff 100644 --- a/cumulusci/tasks/metadata/tests/test_package.py +++ b/cumulusci/tasks/metadata/tests/test_package.py @@ -262,9 +262,7 @@ def test_parser(self): assert """ Test.Test TestMDT - """ == "\n".join( - result - ) + """ == "\n".join(result) def test_parser__missing_item_xpath(self): with pytest.raises(ParserConfigurationError): diff --git a/cumulusci/tasks/metadata_etl/sharing.py b/cumulusci/tasks/metadata_etl/sharing.py index 1989c19bb3..2ce134ae6d 100644 --- a/cumulusci/tasks/metadata_etl/sharing.py +++ b/cumulusci/tasks/metadata_etl/sharing.py @@ -102,7 +102,7 @@ def _poll_action(self): elapsed = datetime.now() - self.time_start if elapsed.total_seconds() > self.options["timeout"]: raise CumulusCIException( - f'Sharing enablement not completed after {self.options["timeout"]} seconds' + f"Sharing enablement not completed after {self.options['timeout']} seconds" ) for sobject in self.owds: diff --git a/cumulusci/tasks/push/README.md b/cumulusci/tasks/push/README.md index a7d7bb8bbd..2d858d6cdd 100644 --- a/cumulusci/tasks/push/README.md +++ b/cumulusci/tasks/push/README.md @@ -1,21 +1,20 @@ # Push Upgrade API Scripts -These scripts are designed to work with the Salesforce Push Upgrade API (in Pilot in Winter 16) which exposes new objects via the Tooling API that allow interacting with push upgrades in a packaging org. The main purpose of these scripts is to use the Push Upgrade API to automate push upgrades through Jenkins. +These scripts are designed to work with the Salesforce Push Upgrade API (in Pilot in Winter 16) which exposes new objects via the Tooling API that allow interacting with push upgrades in a packaging org. The main purpose of these scripts is to use the Push Upgrade API to automate push upgrades through Jenkins. # push_api.py - Python Wrapper for Push Upgrade API -This python file provides wrapper classes around the Tooling API objects and abstracts interaction with them and their related data to make writing scripts easier. All the other scripts in this directory use the SalesforcePushApi wrapper to interact with the Tooling API. +This python file provides wrapper classes around the Tooling API objects and abstracts interaction with them and their related data to make writing scripts easier. All the other scripts in this directory use the SalesforcePushApi wrapper to interact with the Tooling API. Initializing the SalesforcePushApi wrapper can be done with the following python code: push_api = SalesforcePushApi(sf_user, sf_pass, sf_serverurl) You can also pass two optional keyword args to the initialization to control the wrapper's behavior - -* **lazy**: A list of objects that should be lazily looked up. Currently, the only implementations for this are 'jobs' and 'subscribers'. If either are included in the list, they will be looked up on demand when needed by a referenced object. For example, if you are querying all jobs and subscribers is not set to lazy, all subscribers will first be retrieved. If lazy is enabled, subscriber orgs will only be retrieved when trying to resolve references for a particular job. Generally, if you have a lot of subscribers and only expect your script to need to lookup a small number of them, enabling lazy for subscribers will reduce api calls and cause the script to run faster. -* **default_where**: A dictionary with Push Upgrade API objects as key and a value containing a SOQL WHERE clause statement which is applied to all queries against the object to effectively set the universe for a given object. For example: - +- **lazy**: A list of objects that should be lazily looked up. Currently, the only implementations for this are 'jobs' and 'subscribers'. If either are included in the list, they will be looked up on demand when needed by a referenced object. For example, if you are querying all jobs and subscribers is not set to lazy, all subscribers will first be retrieved. If lazy is enabled, subscriber orgs will only be retrieved when trying to resolve references for a particular job. Generally, if you have a lot of subscribers and only expect your script to need to lookup a small number of them, enabling lazy for subscribers will reduce api calls and cause the script to run faster. + +- **default_where**: A dictionary with Push Upgrade API objects as key and a value containing a SOQL WHERE clause statement which is applied to all queries against the object to effectively set the universe for a given object. For example: default_where = {'PackageSubscriber': "OrgType = 'Sandbox'"} In the example above, the wrapper would never return a PackageSubscriber which is not a Sandbox org. @@ -24,22 +23,22 @@ In the example above, the wrapper would never return a PackageSubscriber which i ## Common Environment Variables -The push scripts are all designed to receive their arguments via environment variables. The following are common amongst all of the Push Scripts +The push scripts are all designed to receive their arguments via environment variables. The following are common amongst all of the Push Scripts -* **SF_USERNAME**: The Salesforce username for the packaging org -* **SF_PASSWORD**: The Salesforce password and security token for the packaging org -* **SF_SERVERURL**: The login url for the Salesforce packaging org. +- **SF_USERNAME**: The Salesforce username for the packaging org +- **SF_PASSWORD**: The Salesforce password and security token for the packaging org +- **SF_SERVERURL**: The login url for the Salesforce packaging org. ## get_version_id.py -Takes a namespace and version string and looks up the given version. Returns the version's Salesforce Id. +Takes a namespace and version string and looks up the given version. Returns the version's Salesforce Id. The script handles parsing the version number string into a SOQL query against the MetadataPackageVersion object with the correct MajorVersion, MinorVersion, PatchVersion, ReleaseState, and BuildNumber (i.e. Beta number). ### Required Environment Variables -* **NAMESPACE**: The Package's namespace prefix -* **VERSION_NUMBER**: The version number string. +- **NAMESPACE**: The Package's namespace prefix +- **VERSION_NUMBER**: The version number string. ## orgs_for_push.py @@ -47,13 +46,12 @@ Takes a MetadataPackageVersion Id and optionally a where clause to filter Subscr ### Required Environment Variables -* **VERSION**: The MetadataPackageVersion Id of the version you want to push upgrade. This is used to look for all users not on the version or a newer version +- **VERSION**: The MetadataPackageVersion Id of the version you want to push upgrade. This is used to look for all users not on the version or a newer version ### Optional Environment Variables -* **SUBSCRIBER_WHERE**: An extra filter to be applied to all Subscriber queries. For example, setting this to OrgType = 'Sandbox' would find all Sandbox orgs eligible for push upgrade to the specified version +- **SUBSCRIBER_WHERE**: An extra filter to be applied to all Subscriber queries. For example, setting this to OrgType = 'Sandbox' would find all Sandbox orgs eligible for push upgrade to the specified version ## failed_orgs_for_push.py -Takes a PackagePushRequest Id and optionally a where clause to filter Subscribers and returns a list of OrgId's one per line for all orgs which failed the - +Takes a PackagePushRequest Id and optionally a where clause to filter Subscribers and returns a list of OrgId's one per line for all orgs which failed the diff --git a/cumulusci/tasks/push/pushfails.py b/cumulusci/tasks/push/pushfails.py index 0c5ce23582..74c257a712 100644 --- a/cumulusci/tasks/push/pushfails.py +++ b/cumulusci/tasks/push/pushfails.py @@ -1,4 +1,4 @@ -""" simple task(s) for reporting on push upgrade jobs. +"""simple task(s) for reporting on push upgrade jobs. this doesn't use the nearby push_api module, and was just a quick ccistyle get the job done kinda moment. diff --git a/cumulusci/tasks/push/tasks.py b/cumulusci/tasks/push/tasks.py index ba0255f7c8..39259b7aa1 100644 --- a/cumulusci/tasks/push/tasks.py +++ b/cumulusci/tasks/push/tasks.py @@ -202,7 +202,6 @@ def _report_push_status(self, request_id): class SchedulePushOrgList(BaseSalesforcePushTask): - task_options = { "csv": {"description": "The path to a CSV file to read.", "required": False}, "csv_field_name": { @@ -261,9 +260,9 @@ def _init_options(self, kwargs): if "namespace" not in self.options: self.options["namespace"] = self.project_config.project__package__namespace if "metadata_package_id" not in self.options: - self.options[ - "metadata_package_id" - ] = self.project_config.project__package__metadata_package_id + self.options["metadata_package_id"] = ( + self.project_config.project__package__metadata_package_id + ) if "batch_size" not in self.options: self.options["batch_size"] = 200 if "csv" not in self.options and "csv_field_name" in self.options: @@ -448,10 +447,10 @@ def _get_orgs(self): for included_version in included_versions: # Clear the get_subscribers method cache before each call push_api.get_subscribers.cache_clear() - push_api.default_where[ - "PackageSubscriber" - ] = "{} AND MetadataPackageVersionId = '{}'".format( - default_where["PackageSubscriber"], included_version + push_api.default_where["PackageSubscriber"] = ( + "{} AND MetadataPackageVersionId = '{}'".format( + default_where["PackageSubscriber"], included_version + ) ) for subscriber in push_api.get_subscribers(): orgs.append(subscriber["OrgKey"]) @@ -464,10 +463,10 @@ def _get_orgs(self): excluded_versions = [str(version.sf_id)] for newer in newer_versions: excluded_versions.append(str(newer.sf_id)) - push_api.default_where[ - "PackageSubscriber" - ] += " AND MetadataPackageVersionId NOT IN {}".format( - "('" + "','".join(excluded_versions) + "')" + push_api.default_where["PackageSubscriber"] += ( + " AND MetadataPackageVersionId NOT IN {}".format( + "('" + "','".join(excluded_versions) + "')" + ) ) for subscriber in push_api.get_subscribers(): diff --git a/cumulusci/tasks/release_notes/README.md b/cumulusci/tasks/release_notes/README.md index 9951b15460..4d2e5f50ec 100644 --- a/cumulusci/tasks/release_notes/README.md +++ b/cumulusci/tasks/release_notes/README.md @@ -13,25 +13,25 @@ Start the section with `# Critical Changes` followed by your content For example: This won't be included - + # Critical Changes - + This will be included in Critical Changes - + ## Changes -The Changes section is where you should list off any changes worth highlight to users in the release notes. This section should always include instructions for users for any post-upgrade tasks they need to perform to enable new functionality. For example, users should be told to grant permissions and add new CustomFields to layouts. +The Changes section is where you should list off any changes worth highlight to users in the release notes. This section should always include instructions for users for any post-upgrade tasks they need to perform to enable new functionality. For example, users should be told to grant permissions and add new CustomFields to layouts. Start the section with `# Changes` followed by your content For example: This won't be included - + # Changes - + This will be included in Changes - + ## Issues Closed The Issues Closed section is where you should link to any closed issues that should be listed in the release notes. @@ -41,9 +41,9 @@ Start the section with `# Changes` followed by your content For example: This won't be included - + # Issues Closed - + Fixes #102 resolves #100 This release closes #101 @@ -55,9 +55,9 @@ Would output: #100: Title of Issue 100 #101: Title of Issue 101 #102: Title of Issue 102 - + A few notes about how issues are parsed: -* The parser uses the same format as Github: https://help.github.com/articles/closing-issues-via-commit-messages/ -* The parser searches for all issue numbers and sorts them by their integer value, looks up their title, and outputs a formatted line with the issue number and title for each issue. -* The parser ignores everything else in the line that is not an issue number. Anything that is not an issue number will not appear in the rendered release notes +- The parser uses the same format as Github: https://help.github.com/articles/closing-issues-via-commit-messages/ +- The parser searches for all issue numbers and sorts them by their integer value, looks up their title, and outputs a formatted line with the issue number and title for each issue. +- The parser ignores everything else in the line that is not an issue number. Anything that is not an issue number will not appear in the rendered release notes diff --git a/cumulusci/tasks/release_notes/generator.py b/cumulusci/tasks/release_notes/generator.py index 615665df69..79f4f32b6d 100644 --- a/cumulusci/tasks/release_notes/generator.py +++ b/cumulusci/tasks/release_notes/generator.py @@ -254,7 +254,6 @@ def _update_release_content(self, release, content): # update existing sections for line in release.body.splitlines(): - if current_parser: if current_parser._is_end_line(current_parser._process_line(line)): parser_content = current_parser.render( diff --git a/cumulusci/tasks/release_notes/parser.py b/cumulusci/tasks/release_notes/parser.py index 1fef5d4e16..052a302bdc 100644 --- a/cumulusci/tasks/release_notes/parser.py +++ b/cumulusci/tasks/release_notes/parser.py @@ -60,7 +60,6 @@ def parse(self, change_note): # Add all content once in the section if self._in_section: - # End when the end of section is found if self._is_end_line(line): self._in_section = False diff --git a/cumulusci/tasks/release_notes/task.py b/cumulusci/tasks/release_notes/task.py index 8d56b20776..54eb80221f 100644 --- a/cumulusci/tasks/release_notes/task.py +++ b/cumulusci/tasks/release_notes/task.py @@ -14,7 +14,6 @@ class AllGithubReleaseNotes(BaseGithubTask): - task_options = { "repos": { "description": ( @@ -36,10 +35,10 @@ def _run_task(self): .body ) table_of_contents += ( - f"""
  • {project['repo']}
  • """ + f"""
  • {project["repo"]}
  • """ ) release_project_header = ( - f"""

    {project['repo']}

    """ + f"""

    {project["repo"]}

    """ ) release_html = self.github.markdown( release, @@ -56,7 +55,6 @@ def _run_task(self): class GithubReleaseNotes(BaseGithubTask): - task_options = { "tag": { "description": ( diff --git a/cumulusci/tasks/release_notes/tests/change_notes/full/example1.md b/cumulusci/tasks/release_notes/tests/change_notes/full/example1.md index ac94d5539a..014e63ff40 100644 --- a/cumulusci/tasks/release_notes/tests/change_notes/full/example1.md +++ b/cumulusci/tasks/release_notes/tests/change_notes/full/example1.md @@ -2,7 +2,7 @@ This is my pull request comment. Just explaining some stuff I did here, but shou # Critical Changes -* This will break everything! +- This will break everything! # Changes @@ -10,7 +10,6 @@ Here's something I did. It was really cool Oh yeah I did something else too! - # Issues Closed Fixed #2345 that was a real pain diff --git a/cumulusci/tasks/release_notes/tests/test_generator.py b/cumulusci/tasks/release_notes/tests/test_generator.py index 6bc60baca1..af6a5133f2 100644 --- a/cumulusci/tasks/release_notes/tests/test_generator.py +++ b/cumulusci/tasks/release_notes/tests/test_generator.py @@ -83,7 +83,7 @@ def test_full_content(self): release_notes = DirectoryReleaseNotesGenerator(change_notes_dir) content = release_notes() - expected = "# Critical Changes\r\n\r\n* This will break everything!\r\n\r\n# Changes\r\n\r\nHere's something I did. It was really cool\r\nOh yeah I did something else too!\r\n\r\n# Issues Closed\r\n\r\n#2345\r\n#6236" + expected = "# Critical Changes\r\n\r\n- This will break everything!\r\n\r\n# Changes\r\n\r\nHere's something I did. It was really cool\r\nOh yeah I did something else too!\r\n\r\n# Issues Closed\r\n\r\n#2345\r\n#6236" print(expected) print("-------------------------------------") print(content) @@ -315,8 +315,7 @@ def test_publish_update_content_before_and_after(self): def test_publish_update_content_between(self): tag = "prod/1.4" expected_release_body = ( - "# Critical Changes\r\n\r\nfaz\r\n\r\n" - "# Foo\r\nfoo\r\n# Changes\r\n\r\nfiz" + "# Critical Changes\r\n\r\nfaz\r\n\r\n# Foo\r\nfoo\r\n# Changes\r\n\r\nfiz" ) # mock GitHub API responses self.mock_util.mock_get_repo() @@ -348,8 +347,7 @@ def test_publish_update_content_before_after_and_between(self): self.mock_util.mock_get_release( tag=tag, body=( - "goo\n# Critical Changes\nbar\n" - "# Foo\nfoo\n# Changes\nbiz\n# Zoo\nzoo" + "goo\n# Critical Changes\nbar\n# Foo\nfoo\n# Changes\nbiz\n# Zoo\nzoo" ), ) # create generator diff --git a/cumulusci/tasks/release_notes/tests/test_task.py b/cumulusci/tasks/release_notes/tests/test_task.py index 734a925ddc..277d3bd588 100644 --- a/cumulusci/tasks/release_notes/tests/test_task.py +++ b/cumulusci/tasks/release_notes/tests/test_task.py @@ -76,7 +76,6 @@ def test_run_GithubReleaseNotes_task( class TestParentPullRequestNotes(GithubApiTestMixin): - BUILD_NOTES_LABEL = "Build Change Notes" PARENT_BRANCH_NAME = "feature/long-feature" CHILD_BRANCH_NAME = "feature/long-feature__child-branch" diff --git a/cumulusci/tasks/robotframework/debugger/ui.py b/cumulusci/tasks/robotframework/debugger/ui.py index 31adc0e318..752246d114 100644 --- a/cumulusci/tasks/robotframework/debugger/ui.py +++ b/cumulusci/tasks/robotframework/debugger/ui.py @@ -160,7 +160,7 @@ def do_shell(self, arg): self.builtin.set_test_variable(vars[0], result) print("{} was set to {}".format(vars[0], result), file=self.stdout) else: - for (varname, value) in zip(vars, result): + for varname, value in zip(vars, result): self.builtin.set_test_variable(varname, value) print( "{} was set to {}".format(varname, value), file=self.stdout diff --git a/cumulusci/tasks/robotframework/tests/TestPageObjects.py b/cumulusci/tasks/robotframework/tests/TestPageObjects.py index 974e74bebb..6ce0e2bde4 100644 --- a/cumulusci/tasks/robotframework/tests/TestPageObjects.py +++ b/cumulusci/tasks/robotframework/tests/TestPageObjects.py @@ -1,6 +1,7 @@ """ this is the docstring """ + from cumulusci.robotframework.pageobjects import DetailPage, ListingPage, pageobject TITLE = "This is the title" diff --git a/cumulusci/tasks/robotframework/tests/test_debugger.py b/cumulusci/tasks/robotframework/tests/test_debugger.py index 63446075ad..232a48cbfd 100644 --- a/cumulusci/tasks/robotframework/tests/test_debugger.py +++ b/cumulusci/tasks/robotframework/tests/test_debugger.py @@ -64,9 +64,9 @@ def test_listener_step(self): self.listener.start_test("Test 1", {"longname": "Root.example.Test 1"}) self.listener.start_keyword("cumulusci.Salesforce.breakpoint", {}) - assert ( - len(self.listener.breakpoints) == 1 - ), "Weird. There should have only been a single breakpoint" + assert len(self.listener.breakpoints) == 1, ( + "Weird. There should have only been a single breakpoint" + ) # call `do_step` of the *listener*, not the debugger UI. # the debugger ui "do_step" method will both add a new breakpoint # and then continue to that breakpoint, and then that breakpoint @@ -76,9 +76,9 @@ def test_listener_step(self): # The 'step' command should cause a new temporary breakpoint to be added # in the same context as the current keyword. - assert ( - len(self.listener.breakpoints) == 2 - ), "Expected a breakpoint to be added on the 'step' command" + assert len(self.listener.breakpoints) == 2, ( + "Expected a breakpoint to be added on the 'step' command" + ) assert self.listener.breakpoints[-1].pattern == "Root.example.Test 1::*" assert self.listener.breakpoints[-1].temporary @@ -100,9 +100,9 @@ def test_temporary_breakpoint(self): listener.start_test("Test Case", attrs={}) listener.start_keyword("temporary breakpoint", attrs={"args": ["one", "two"]}) assert len(listener.breakpoints) == 1 - assert ( - listener.breakpoints[0].pattern == "*::breakpoint" - ), "the wrong breakpoint was removed" + assert listener.breakpoints[0].pattern == "*::breakpoint", ( + "the wrong breakpoint was removed" + ) listener.rdb.cmdloop.assert_called_once() @@ -235,7 +235,6 @@ def test_shell_one_variable(self): with mock.patch.object( self.mock_builtin, "run_keyword_and_ignore_error", return_value=("PASS", 42) ): - return_value = self.cli.do_shell( "${value} get variable value ${whatever}" ) @@ -352,9 +351,9 @@ def test_suite(self): def test_breakpoint_match(self): bp = debugger.Breakpoint(debugger.Keyword, "*::breakpoint") - assert bp.match( - context="Suite.Test Case::breakpoint" - ), "expected breakpoint to match, but it didn't" - assert not bp.match( - context="Suite.Test Case::some other keyword" - ), "didn't expect breakpoint to match, but it did" + assert bp.match(context="Suite.Test Case::breakpoint"), ( + "expected breakpoint to match, but it didn't" + ) + assert not bp.match(context="Suite.Test Case::some other keyword"), ( + "didn't expect breakpoint to match, but it did" + ) diff --git a/cumulusci/tasks/robotframework/tests/test_robotframework.py b/cumulusci/tasks/robotframework/tests/test_robotframework.py index 981fc744bc..36187fddd9 100644 --- a/cumulusci/tasks/robotframework/tests/test_robotframework.py +++ b/cumulusci/tasks/robotframework/tests/test_robotframework.py @@ -199,7 +199,8 @@ def test_default_listeners(self): # first, verify that not specifying any listener options # results in no listeners... task = create_task( - Robot, {"suites": "test"} # required, or the task will raise an exception + Robot, + {"suites": "test"}, # required, or the task will raise an exception ) assert len(task.options["options"]["listener"]) == 0 @@ -227,9 +228,9 @@ def test_debug_option(self): listener_classes = [ listener.__class__ for listener in task.options["options"]["listener"] ] - assert ( - DebugListener in listener_classes - ), "DebugListener was not in task options" + assert DebugListener in listener_classes, ( + "DebugListener was not in task options" + ) def test_verbose_option(self): """Verify that setting verbose to True attaches the appropriate listener""" @@ -243,9 +244,9 @@ def test_verbose_option(self): listener_classes = [ listener.__class__ for listener in task.options["options"]["listener"] ] - assert ( - KeywordLogger in listener_classes - ), "KeywordLogger was not in task options" + assert KeywordLogger in listener_classes, ( + "KeywordLogger was not in task options" + ) def test_user_defined_listeners_option(self): """Verify that our listeners don't replace user-defined listeners""" @@ -485,54 +486,73 @@ def test_csv(self): reader = csv.reader(csvfile) actual_output = [row for row in reader] + def _resolved_source(path: str) -> str: + """Normalize RobotLibDoc CSV `Source` paths across cwd layouts. + + Robot may emit absolute paths, cwd-relative paths, or repo-root-relative + paths that include `.worktrees/...` segments. For assertions, map + everything to a stable absolute path rooted at the current working + directory + `cumulusci/tasks/robotframework/tests/`. + """ + + normalized = path.replace("\\", "/") + tail = "cumulusci/tasks/robotframework/tests/" + if tail in normalized: + rest = normalized.split(tail, 1)[1] + return str((Path.cwd() / tail / rest).resolve()) + return str(Path(path).resolve()) + # not only does this verify that the expected keywords are in # the output, but that the base class keywords are *not* - datadir = os.path.join("cumulusci", "tasks", "robotframework", "tests", "") + tests_dir = Path(__file__).resolve().parent + page_objects = str(tests_dir / "TestPageObjects.py") + test_library = str(tests_dir / "TestLibrary.py") + test_resource = str(tests_dir / "TestResource.robot") expected_output = [ ["Name", "Source", "Line#", "po type", "po_object", "Documentation"], [ "Keyword One", - f"{datadir}TestPageObjects.py", - "13", + page_objects, + "14", "Listing", "Something__c", "", ], [ "Keyword One", - f"{datadir}TestPageObjects.py", - "24", + page_objects, + "25", "Detail", "Something__c", "", ], [ "Keyword Three", - f"{datadir}TestPageObjects.py", - "30", + page_objects, + "31", "Detail", "Something__c", "", ], [ "Keyword Two", - f"{datadir}TestPageObjects.py", - "16", + page_objects, + "17", "Listing", "Something__c", "", ], [ "Keyword Two", - f"{datadir}TestPageObjects.py", - "27", + page_objects, + "28", "Detail", "Something__c", "", ], [ "Library Keyword One", - f"{datadir}TestLibrary.py", + test_library, "13", "", "", @@ -540,7 +560,7 @@ def test_csv(self): ], [ "Library Keyword Two", - f"{datadir}TestLibrary.py", + test_library, "17", "", "", @@ -548,7 +568,7 @@ def test_csv(self): ], [ "Resource keyword one", - f"{datadir}TestResource.robot", + test_resource, "2", "", "", @@ -556,7 +576,7 @@ def test_csv(self): ], [ "Resource keyword two", - f"{datadir}TestResource.robot", + test_resource, "6", "", "", @@ -564,7 +584,13 @@ def test_csv(self): ], ] - assert actual_output == expected_output + normalized_actual = [ + [row[0], _resolved_source(row[1]), *row[2:]] for row in actual_output + ] + normalized_expected = [ + [row[0], _resolved_source(row[1]), *row[2:]] for row in expected_output + ] + assert normalized_actual == normalized_expected @mock.patch("cumulusci.tasks.robotframework.libdoc.view_file") def test_preview_option(self, mock_view_file): @@ -771,9 +797,11 @@ def _run_robot_and_parse_xml( ): universal_config = UniversalConfig() project_config = BaseProjectConfig(universal_config) - with temporary_dir() as d, mock.patch( - "cumulusci.robotframework.Salesforce.Salesforce._init_locators" - ), responses.RequestsMock(): + with ( + temporary_dir() as d, + mock.patch("cumulusci.robotframework.Salesforce.Salesforce._init_locators"), + responses.RequestsMock(), + ): project_config.repo_info["root"] = d suite = Path(self.datadir) / "../../../robotframework/" / suite_path task = create_task( diff --git a/cumulusci/tasks/salesforce/CreatePackage.py b/cumulusci/tasks/salesforce/CreatePackage.py index 4b302ff9c7..50701367af 100644 --- a/cumulusci/tasks/salesforce/CreatePackage.py +++ b/cumulusci/tasks/salesforce/CreatePackage.py @@ -19,9 +19,9 @@ def _init_options(self, kwargs): if "package" not in self.options: self.options["package"] = self.project_config.project__package__name if "api_version" not in self.options: - self.options[ - "api_version" - ] = self.project_config.project__package__api_version + self.options["api_version"] = ( + self.project_config.project__package__api_version + ) def _get_package_zip(self, path=None): return CreatePackageZipBuilder( diff --git a/cumulusci/tasks/salesforce/DeployBundles.py b/cumulusci/tasks/salesforce/DeployBundles.py index 072cd51cb5..a71c39a7cd 100644 --- a/cumulusci/tasks/salesforce/DeployBundles.py +++ b/cumulusci/tasks/salesforce/DeployBundles.py @@ -4,9 +4,9 @@ from cumulusci.tasks.salesforce import Deploy deploy_options = copy.deepcopy(Deploy.task_options) -deploy_options["path"][ - "description" -] = "The path to the parent directory containing the metadata bundles directories" +deploy_options["path"]["description"] = ( + "The path to the parent directory containing the metadata bundles directories" +) class DeployBundles(Deploy): diff --git a/cumulusci/tasks/salesforce/DescribeMetadataTypes.py b/cumulusci/tasks/salesforce/DescribeMetadataTypes.py index 8c0e237e80..b9768619fc 100644 --- a/cumulusci/tasks/salesforce/DescribeMetadataTypes.py +++ b/cumulusci/tasks/salesforce/DescribeMetadataTypes.py @@ -13,9 +13,9 @@ class DescribeMetadataTypes(BaseRetrieveMetadata): def _init_options(self, kwargs): super(DescribeMetadataTypes, self)._init_options(kwargs) if "api_version" not in self.options: - self.options[ - "api_version" - ] = self.project_config.project__package__api_version + self.options["api_version"] = ( + self.project_config.project__package__api_version + ) def _get_api(self): return self.api_class(self, self.options.get("api_version")) diff --git a/cumulusci/tasks/salesforce/RetrieveReportsAndDashboards.py b/cumulusci/tasks/salesforce/RetrieveReportsAndDashboards.py index 515485364b..556f8e364c 100644 --- a/cumulusci/tasks/salesforce/RetrieveReportsAndDashboards.py +++ b/cumulusci/tasks/salesforce/RetrieveReportsAndDashboards.py @@ -28,9 +28,9 @@ class RetrieveReportsAndDashboards(BaseRetrieveMetadata): def _init_options(self, kwargs): super(RetrieveReportsAndDashboards, self)._init_options(kwargs) if "api_version" not in self.options: - self.options[ - "api_version" - ] = self.project_config.project__package__api_version + self.options["api_version"] = ( + self.project_config.project__package__api_version + ) def _validate_options(self): super(RetrieveReportsAndDashboards, self)._validate_options() diff --git a/cumulusci/tasks/salesforce/UninstallLocalNamespacedBundles.py b/cumulusci/tasks/salesforce/UninstallLocalNamespacedBundles.py index 9c2c23d67b..226f932c9d 100644 --- a/cumulusci/tasks/salesforce/UninstallLocalNamespacedBundles.py +++ b/cumulusci/tasks/salesforce/UninstallLocalNamespacedBundles.py @@ -4,7 +4,6 @@ class UninstallLocalNamespacedBundles(UninstallLocalBundles): - task_options = { "path": { "description": "The path to a directory containing the metadata bundles (subdirectories) to uninstall", diff --git a/cumulusci/tasks/salesforce/UninstallPackaged.py b/cumulusci/tasks/salesforce/UninstallPackaged.py index 0032d9d83e..095f0d7068 100644 --- a/cumulusci/tasks/salesforce/UninstallPackaged.py +++ b/cumulusci/tasks/salesforce/UninstallPackaged.py @@ -4,7 +4,6 @@ class UninstallPackaged(UninstallLocal): - task_options = { "package": { "description": "The package name to uninstall. All metadata from the package will be retrieved and a custom destructiveChanges.xml package will be constructed and deployed to delete all deleteable metadata from the package. Defaults to project__package__name", diff --git a/cumulusci/tasks/salesforce/activate_flow.py b/cumulusci/tasks/salesforce/activate_flow.py index 208ab62ea6..a0b0a9a03a 100644 --- a/cumulusci/tasks/salesforce/activate_flow.py +++ b/cumulusci/tasks/salesforce/activate_flow.py @@ -50,7 +50,7 @@ def _run_task(self): results = [] for listed_flow in result["records"]: results.append(listed_flow["DeveloperName"]) - self.logger.info(f'Processing: {listed_flow["DeveloperName"]}') + self.logger.info(f"Processing: {listed_flow['DeveloperName']}") path = f"tooling/sobjects/FlowDefinition/{listed_flow['Id']}" urlpath = self.sf.base_url + path diff --git a/cumulusci/tasks/salesforce/custom_settings_wait.py b/cumulusci/tasks/salesforce/custom_settings_wait.py index 77432d5517..8941a684f5 100644 --- a/cumulusci/tasks/salesforce/custom_settings_wait.py +++ b/cumulusci/tasks/salesforce/custom_settings_wait.py @@ -1,4 +1,4 @@ -""" a task for waiting on a specific custom settings value """ +"""a task for waiting on a specific custom settings value""" from simple_salesforce.exceptions import SalesforceError diff --git a/cumulusci/tasks/salesforce/install_package_version.py b/cumulusci/tasks/salesforce/install_package_version.py index 13c95c9920..b920f5af00 100644 --- a/cumulusci/tasks/salesforce/install_package_version.py +++ b/cumulusci/tasks/salesforce/install_package_version.py @@ -123,16 +123,16 @@ def _init_options(self, kwargs): dependency.package_dependency, PackageNamespaceVersionDependency ): self.options["version"] = dependency.package_dependency.version - self.options[ - "version_id" - ] = dependency.package_dependency.version_id + self.options["version_id"] = ( + dependency.package_dependency.version_id + ) elif isinstance( dependency.package_dependency, PackageVersionIdDependency ): self.options["version"] = dependency.package_dependency.version_id - self.options[ - "version_number" - ] = dependency.package_dependency.version_number + self.options["version_number"] = ( + dependency.package_dependency.version_number + ) else: raise CumulusCIException( f"The release for {version} does not identify a package version." diff --git a/cumulusci/tasks/salesforce/network_member_group.py b/cumulusci/tasks/salesforce/network_member_group.py index 5a96c121e0..aafd73e3eb 100644 --- a/cumulusci/tasks/salesforce/network_member_group.py +++ b/cumulusci/tasks/salesforce/network_member_group.py @@ -27,8 +27,7 @@ class CreateNetworkMemberGroups(BaseSalesforceApiTask): }, "profile_names": { "description": ( - "List of Profile Names to add as NetworkMemberGroups " - "for this Network." + "List of Profile Names to add as NetworkMemberGroups for this Network." ), "required": False, }, diff --git a/cumulusci/tasks/salesforce/nonsourcetracking.py b/cumulusci/tasks/salesforce/nonsourcetracking.py index 6350fdf7e4..bcb79dd6a1 100644 --- a/cumulusci/tasks/salesforce/nonsourcetracking.py +++ b/cumulusci/tasks/salesforce/nonsourcetracking.py @@ -18,7 +18,6 @@ class ListNonSourceTrackable(BaseSalesforceApiTask): - task_options = { "api_version": { "description": "Override the API version used to list metadatatypes", @@ -28,9 +27,9 @@ class ListNonSourceTrackable(BaseSalesforceApiTask): def _init_task(self): super()._init_task() if "api_version" not in self.options: - self.options[ - "api_version" - ] = self.project_config.project__package__api_version + self.options["api_version"] = ( + self.project_config.project__package__api_version + ) def get_types_details(self, api_version): # The Metadata coverage report: https://developer.salesforce.com/docs/metadata-coverage/{version} is created from @@ -96,9 +95,9 @@ def _init_task(self): def _init_options(self, kwargs): super(ListComponents, self)._init_options(kwargs) if "api_version" not in self.options: - self.options[ - "api_version" - ] = self.project_config.project__package__api_version + self.options["api_version"] = ( + self.project_config.project__package__api_version + ) if "metadata_types" not in self.options: self.options["metadata_types"] = ListNonSourceTrackable( org_config=self.org_config, @@ -154,9 +153,9 @@ def _run_task(self): retrieve_components_task_options["exclude"] = { "description": "Exclude components matching this name." } -retrieve_components_task_options[ - "namespace_tokenize" -] = BaseRetrieveMetadata.task_options["namespace_tokenize"] +retrieve_components_task_options["namespace_tokenize"] = ( + BaseRetrieveMetadata.task_options["namespace_tokenize"] +) class RetrieveComponents(ListComponents, BaseSalesforceApiTask): diff --git a/cumulusci/tasks/salesforce/org_settings.py b/cumulusci/tasks/salesforce/org_settings.py index 829320453e..51a8898d6a 100644 --- a/cumulusci/tasks/salesforce/org_settings.py +++ b/cumulusci/tasks/salesforce/org_settings.py @@ -156,8 +156,8 @@ def _get_object_file(object_name: str, settings: dict) -> str: """ record_type = f""" - {capitalize(settings['defaultRecordType'])} - + {capitalize(settings["defaultRecordType"])} + true {business_process_reference} diff --git a/cumulusci/tasks/salesforce/salesforce_files.py b/cumulusci/tasks/salesforce/salesforce_files.py index be39322ca7..187292efdc 100644 --- a/cumulusci/tasks/salesforce/salesforce_files.py +++ b/cumulusci/tasks/salesforce/salesforce_files.py @@ -72,9 +72,7 @@ def _run_task(self): file_list = self.options["file_list"] - if ( - file_list - ): # If the list of names of files to be downloaded is specified, fetch only those files. + if file_list: # If the list of names of files to be downloaded is specified, fetch only those files. items_list = [item.strip() for item in file_list.split(",")] conditions = [] for item in items_list: @@ -99,7 +97,7 @@ def _run_task(self): self.logger.info(f"Found {len(available_files)} files in the org.\n") self.logger.info( - f'Files will be downloaded in the directory: {self.options["path"]} \n' + f"Files will be downloaded in the directory: {self.options['path']} \n" ) for current_file in available_files: diff --git a/cumulusci/tasks/salesforce/sourcetracking.py b/cumulusci/tasks/salesforce/sourcetracking.py index 903cd3595c..43d4bc8ffc 100644 --- a/cumulusci/tasks/salesforce/sourcetracking.py +++ b/cumulusci/tasks/salesforce/sourcetracking.py @@ -413,9 +413,9 @@ def _init_options(self, kwargs): self.options["output_dir"] = output_dir if "api_version" not in self.options: - self.options[ - "api_version" - ] = self.project_config.project__package__api_version + self.options["api_version"] = ( + self.project_config.project__package__api_version + ) def _run_task(self): self._load_snapshot() @@ -464,7 +464,6 @@ def _run_task(self): class SnapshotChanges(ListChanges): - task_options = {} def _init_options(self, kwargs): diff --git a/cumulusci/tasks/salesforce/tests/test_PackageUpload.py b/cumulusci/tasks/salesforce/tests/test_PackageUpload.py index 6af96ce583..bfcaaa8be5 100644 --- a/cumulusci/tasks/salesforce/tests/test_PackageUpload.py +++ b/cumulusci/tasks/salesforce/tests/test_PackageUpload.py @@ -109,7 +109,6 @@ def generate_valid_version_options( asserted_minor_version: str, is_negative: bool = False, ): - """This is function is used to generate test cases for test_validate_versions(positive as well as negative) Arguments: major_version:acutal major version which is to be passed none if not passed. diff --git a/cumulusci/tasks/salesforce/tests/test_check_components.py b/cumulusci/tasks/salesforce/tests/test_check_components.py index b7f963cab0..1110a8e7ea 100644 --- a/cumulusci/tasks/salesforce/tests/test_check_components.py +++ b/cumulusci/tasks/salesforce/tests/test_check_components.py @@ -259,18 +259,18 @@ def test_collect_components_from_paths( mock_tree = MagicMock() mock_tree.findall.return_value = [ MagicMock( - findall=lambda tag: [MagicMock(text="Delivery")] - if tag == "members" - else [], + findall=lambda tag: ( + [MagicMock(text="Delivery")] if tag == "members" else [] + ), find=lambda tag: MagicMock(text="ApexClass") if tag == "name" else None, ), MagicMock( - findall=lambda tag: [MagicMock(text="Delivery__c")] - if tag == "members" - else [], - find=lambda tag: MagicMock(text="CustomObject") - if tag == "name" - else None, + findall=lambda tag: ( + [MagicMock(text="Delivery__c")] if tag == "members" else [] + ), + find=lambda tag: ( + MagicMock(text="CustomObject") if tag == "name" else None + ), ), ] mock_tree.find.return_value = MagicMock(text="58.0") diff --git a/cumulusci/tasks/salesforce/tests/test_network_member_group.py b/cumulusci/tasks/salesforce/tests/test_network_member_group.py index cf210b6c6c..4573cec913 100644 --- a/cumulusci/tasks/salesforce/tests/test_network_member_group.py +++ b/cumulusci/tasks/salesforce/tests/test_network_member_group.py @@ -170,7 +170,6 @@ def test_process_parent__no_names(self): None, [], ]: - task = create_task( CreateNetworkMemberGroups, {"network_name": network_name} ) diff --git a/cumulusci/tasks/salesforce/tests/test_org_settings.py b/cumulusci/tasks/salesforce/tests/test_org_settings.py index 8ce6b34f89..e63c945fe8 100644 --- a/cumulusci/tasks/salesforce/tests/test_org_settings.py +++ b/cumulusci/tasks/salesforce/tests/test_org_settings.py @@ -376,9 +376,7 @@ def test_build_settings_package(self): true -""".translate( - rm_ws - ) +""".translate(rm_ws) ) assert ( (pathlib.Path(path) / "objects" / "Solution.object") @@ -401,7 +399,5 @@ def test_build_settings_package(self): Draft -""".translate( - rm_ws - ) +""".translate(rm_ws) ) diff --git a/cumulusci/tasks/salesforce/tests/test_retrieve_profile.py b/cumulusci/tasks/salesforce/tests/test_retrieve_profile.py index 2983b4ba32..cb2aa71441 100644 --- a/cumulusci/tasks/salesforce/tests/test_retrieve_profile.py +++ b/cumulusci/tasks/salesforce/tests/test_retrieve_profile.py @@ -178,7 +178,7 @@ def test_save_profile_file_existing(retrieve_profile_task, tmpdir): def test_add_flow_accesses(retrieve_profile_task): - profile_content = "\n" " Hello\n" "" + profile_content = "\n Hello\n" flows = ["Flow1", "Flow2"] expected_content = ( "\n" @@ -197,7 +197,7 @@ def test_add_flow_accesses(retrieve_profile_task): assert modified_content == expected_content # Content without the tag - profile_content = "\n" " Hello\n" + profile_content = "\n Hello\n" modified_content = retrieve_profile_task.add_flow_accesses(profile_content, flows) assert modified_content == profile_content @@ -206,16 +206,17 @@ def test_run_task(retrieve_profile_task, tmpdir, caplog): retrieve_profile_task.extract_dir = tmpdir temp_zipfile = create_temp_zip_file() - with patch.object( - RetrieveProfileApi, - "_retrieve_permissionable_entities", - return_value=({"ApexClass": ["TestApexClass"]}, {"Profile1": ["Flow1"]}), - ), patch.object( - RetrieveProfileApi, "_init_task", return_value="something" - ), patch.object( - ApiRetrieveUnpackaged, "__call__", return_value=temp_zipfile - ), patch.object( - RetrieveProfileApi, "_retrieve_existing_profiles", return_value=["Profile1"] + with ( + patch.object( + RetrieveProfileApi, + "_retrieve_permissionable_entities", + return_value=({"ApexClass": ["TestApexClass"]}, {"Profile1": ["Flow1"]}), + ), + patch.object(RetrieveProfileApi, "_init_task", return_value="something"), + patch.object(ApiRetrieveUnpackaged, "__call__", return_value=temp_zipfile), + patch.object( + RetrieveProfileApi, "_retrieve_existing_profiles", return_value=["Profile1"] + ), ): retrieve_profile_task._run_task() diff --git a/cumulusci/tasks/salesforce/tests/test_salesforce_files.py b/cumulusci/tasks/salesforce/tests/test_salesforce_files.py index e236552fa0..67cb2678a1 100644 --- a/cumulusci/tasks/salesforce/tests/test_salesforce_files.py +++ b/cumulusci/tasks/salesforce/tests/test_salesforce_files.py @@ -148,10 +148,13 @@ def test_run_task(self, mock_open, mock_isfile, mock_listdir, mock_post): # Mock file discovery mock_listdir.return_value = ["file1.txt", "file2.txt"] - mock_isfile.side_effect = lambda filepath: filepath in [ - os.path.join("test_dir", "file1.txt"), - os.path.join("test_dir", "file2.txt"), - ] + mock_isfile.side_effect = lambda filepath: ( + filepath + in [ + os.path.join("test_dir", "file1.txt"), + os.path.join("test_dir", "file2.txt"), + ] + ) # Mock requests response mock_response = Mock() diff --git a/cumulusci/tasks/salesforce/users/tests/test_photos.py b/cumulusci/tasks/salesforce/users/tests/test_photos.py index 1b97dbcd41..4552d3947f 100644 --- a/cumulusci/tasks/salesforce/users/tests/test_photos.py +++ b/cumulusci/tasks/salesforce/users/tests/test_photos.py @@ -189,12 +189,12 @@ def test_insert_content_document__exception_creating_content_document(self): # Copy photo to temporary direcory. shutil.copy(str(self.photo_path), d) temp_photo_path = pathlib.Path(os.path.join(d, self.photo)).resolve() - assert ( - temp_photo_path.exists() - ), "photo mock was not copied to the temporary directory" - assert ( - temp_photo_path.is_file() - ), "Path the the photo mock in the temporary directory should point to a file" + assert temp_photo_path.exists(), ( + "photo mock was not copied to the temporary directory" + ) + assert temp_photo_path.is_file(), ( + "Path the the photo mock in the temporary directory should point to a file" + ) self.task.sf.ContentVersion.create._expected_calls = [ mock.call( @@ -247,12 +247,12 @@ def test_insert_content_document__success(self): # Copy photo to temporary direcory. shutil.copy(str(self.photo_path), d) temp_photo_path = pathlib.Path(os.path.join(d, self.photo)).resolve() - assert ( - temp_photo_path.exists() - ), "photo mock was not copied to the temporary directory" - assert ( - temp_photo_path.is_file() - ), "Path the the photo mock in the temporary directory should point to a file" + assert temp_photo_path.exists(), ( + "photo mock was not copied to the temporary directory" + ) + assert temp_photo_path.is_file(), ( + "Path the the photo mock in the temporary directory should point to a file" + ) self.task.sf.ContentVersion.create._expected_calls = [ mock.call( diff --git a/cumulusci/tasks/sample_data/capture_sample_data.py b/cumulusci/tasks/sample_data/capture_sample_data.py index 33785263bc..483b7d7acf 100644 --- a/cumulusci/tasks/sample_data/capture_sample_data.py +++ b/cumulusci/tasks/sample_data/capture_sample_data.py @@ -50,18 +50,21 @@ def _run_task(self): if not loading_rules.exists(): raise TaskOptionsError(f"Cannot find {loading_rules}") - with get_org_schema( - self.sf, - self.org_config, - include_counts=True, - filters=[Filters.extractable, Filters.createable], - ) as schema, Dataset( - name, - self.project_config, - self.sf, - self.org_config, - schema, - ) as dataset: + with ( + get_org_schema( + self.sf, + self.org_config, + include_counts=True, + filters=[Filters.extractable, Filters.createable], + ) as schema, + Dataset( + name, + self.project_config, + self.sf, + self.org_config, + schema, + ) as dataset, + ): if not dataset.path.exists(): dataset.create() verb = "Created" diff --git a/cumulusci/tasks/sample_data/load_sample_data.py b/cumulusci/tasks/sample_data/load_sample_data.py index c89fb5bf85..fd980edf0f 100644 --- a/cumulusci/tasks/sample_data/load_sample_data.py +++ b/cumulusci/tasks/sample_data/load_sample_data.py @@ -30,18 +30,21 @@ def _run_task(self): name = self._find_dataset() if not name: return - with get_org_schema( - self.sf, - self.org_config, - include_counts=True, - filters=[Filters.extractable, Filters.createable], - ) as schema, Dataset( - name, - self.project_config, - self.sf, - self.org_config, - schema, - ) as dataset: + with ( + get_org_schema( + self.sf, + self.org_config, + include_counts=True, + filters=[Filters.extractable, Filters.createable], + ) as schema, + Dataset( + name, + self.project_config, + self.sf, + self.org_config, + schema, + ) as dataset, + ): self.return_values = dataset.load(self.options, self.logger) or {} return self.return_values diff --git a/cumulusci/tasks/sample_data/test_capture_sample_data.py b/cumulusci/tasks/sample_data/test_capture_sample_data.py index d4819de50a..c46730e4f8 100644 --- a/cumulusci/tasks/sample_data/test_capture_sample_data.py +++ b/cumulusci/tasks/sample_data/test_capture_sample_data.py @@ -23,18 +23,20 @@ def setup_test(org_config): describe_for("Contact"), describe_for("Opportunity"), ) - with mock.patch.object( - type(org_config), "is_person_accounts_enabled", False - ), mock.patch( - "cumulusci.tasks.sample_data.capture_sample_data.get_org_schema", - lambda _sf, org_config, **kwargs: _fake_get_org_schema( - org_config, - obj_describes, - object_counts, - included_objects=["Account", "Contact", "Opportunity"], - **kwargs, + with ( + mock.patch.object(type(org_config), "is_person_accounts_enabled", False), + mock.patch( + "cumulusci.tasks.sample_data.capture_sample_data.get_org_schema", + lambda _sf, org_config, **kwargs: _fake_get_org_schema( + org_config, + obj_describes, + object_counts, + included_objects=["Account", "Contact", "Opportunity"], + **kwargs, + ), ), - ), responses.RequestsMock() as rsps: + responses.RequestsMock() as rsps, + ): rsps.add( responses.GET, f"{org_config.instance_url}/services/data/v{CURRENT_SF_API_VERSION}/tooling/sobjects", diff --git a/cumulusci/tasks/sample_data/test_load_sample_data.py b/cumulusci/tasks/sample_data/test_load_sample_data.py index 0de260ec97..99da956419 100644 --- a/cumulusci/tasks/sample_data/test_load_sample_data.py +++ b/cumulusci/tasks/sample_data/test_load_sample_data.py @@ -21,16 +21,17 @@ def setup_test(org_config): describe_for("Contact"), describe_for("Opportunity"), ) - with mock.patch.object( - type(org_config), "is_person_accounts_enabled", False - ), mock.patch( - "cumulusci.tasks.sample_data.load_sample_data.get_org_schema", - lambda _sf, org_config, **kwargs: _fake_get_org_schema( - org_config, - obj_describes, - object_counts, - included_objects=["Account", "Contact", "Opportunity"], - **kwargs, + with ( + mock.patch.object(type(org_config), "is_person_accounts_enabled", False), + mock.patch( + "cumulusci.tasks.sample_data.load_sample_data.get_org_schema", + lambda _sf, org_config, **kwargs: _fake_get_org_schema( + org_config, + obj_describes, + object_counts, + included_objects=["Account", "Contact", "Opportunity"], + **kwargs, + ), ), ): yield diff --git a/cumulusci/tasks/sfdx.py b/cumulusci/tasks/sfdx.py index 717fc26570..aaa1e20373 100644 --- a/cumulusci/tasks/sfdx.py +++ b/cumulusci/tasks/sfdx.py @@ -1,4 +1,4 @@ -""" Wrapper tasks for the SFDX CLI +"""Wrapper tasks for the SFDX CLI TODO: Instead of everyone overriding random attrs, especially as future @@ -11,6 +11,7 @@ Then here in SFDX we will add an additional metalayer for how the CLI formats args opts commands etc. """ + import json from cumulusci.core.config import ScratchOrgConfig diff --git a/cumulusci/tasks/tests/test_command.py b/cumulusci/tasks/tests/test_command.py index 128353f08c..153e49415c 100644 --- a/cumulusci/tasks/tests/test_command.py +++ b/cumulusci/tasks/tests/test_command.py @@ -1,5 +1,4 @@ -""" Tests for the Command tasks """ - +"""Tests for the Command tasks""" from io import BytesIO from unittest import mock diff --git a/cumulusci/tasks/tests/test_connectedapp.py b/cumulusci/tasks/tests/test_connectedapp.py index 81e6f765eb..af03c22d79 100644 --- a/cumulusci/tasks/tests/test_connectedapp.py +++ b/cumulusci/tasks/tests/test_connectedapp.py @@ -1,4 +1,4 @@ -""" Tests for the connectedapp tasks """ +"""Tests for the connectedapp tasks""" import os import re diff --git a/cumulusci/tasks/tests/test_datadictionary.py b/cumulusci/tasks/tests/test_datadictionary.py index 6d98770620..e0a50538b6 100644 --- a/cumulusci/tasks/tests/test_datadictionary.py +++ b/cumulusci/tasks/tests/test_datadictionary.py @@ -1532,9 +1532,11 @@ def fake_get_static_dependencies( strategies=None, filter_function=None, ): - filter_function( - GitHubDynamicDependency(github="https://github.com/test/test") - ), + ( + filter_function( + GitHubDynamicDependency(github="https://github.com/test/test") + ), + ) filter_function( GitHubDynamicDependency(github="https://github.com/test1/test1") ) diff --git a/cumulusci/tasks/tests/test_sfdx.py b/cumulusci/tasks/tests/test_sfdx.py index a2b5a46a81..5f245b21ec 100644 --- a/cumulusci/tasks/tests/test_sfdx.py +++ b/cumulusci/tasks/tests/test_sfdx.py @@ -1,5 +1,4 @@ -""" Tests for the SFDX Command Wrapper""" - +"""Tests for the SFDX Command Wrapper""" from unittest import mock from unittest.mock import MagicMock, patch diff --git a/cumulusci/tests/pytest_plugins/pytest_sf_orgconnect.py b/cumulusci/tests/pytest_plugins/pytest_sf_orgconnect.py index d226549110..3a011ae3f2 100644 --- a/cumulusci/tests/pytest_plugins/pytest_sf_orgconnect.py +++ b/cumulusci/tests/pytest_plugins/pytest_sf_orgconnect.py @@ -122,9 +122,10 @@ def org_config(request, current_org_shape, cli_org_config, fallback_org_config): # fast running test suites it might return a hardcoded # org and for integration test suites it might return # a specific default org or throw an exception. - with mock.patch.object( - OrgConfig, "latest_api_version", CURRENT_SF_API_VERSION - ), mock.patch.object(OrgConfig, "refresh_oauth_token"): + with ( + mock.patch.object(OrgConfig, "latest_api_version", CURRENT_SF_API_VERSION), + mock.patch.object(OrgConfig, "refresh_oauth_token"), + ): yield org_config diff --git a/cumulusci/tests/pytest_plugins/vcr_string_compressor.py b/cumulusci/tests/pytest_plugins/vcr_string_compressor.py index 283257da77..7a904d1b28 100644 --- a/cumulusci/tests/pytest_plugins/vcr_string_compressor.py +++ b/cumulusci/tests/pytest_plugins/vcr_string_compressor.py @@ -14,7 +14,6 @@ Note that the status can be elided because it defaults to Closed. """ - import re import typing as T from pathlib import Path diff --git a/cumulusci/tests/test_integration_infrastructure.py b/cumulusci/tests/test_integration_infrastructure.py index 2b8917f4ba..6de01e8d80 100644 --- a/cumulusci/tests/test_integration_infrastructure.py +++ b/cumulusci/tests/test_integration_infrastructure.py @@ -74,9 +74,9 @@ def test_file_was_not_created(self): @pytest.mark.needs_org() def test_cli_specified_org(self, capture_orgid_using_task): self.__class__.remembered_cli_specified_org_id = capture_orgid_using_task() - assert self.remembered_cli_specified_org_id.startswith( - "00D" - ), self.__class__.remembered_cli_specified_org_id + assert self.remembered_cli_specified_org_id.startswith("00D"), ( + self.__class__.remembered_cli_specified_org_id + ) @pytest.mark.needs_org() @pytest.mark.slow() diff --git a/cumulusci/tests/util.py b/cumulusci/tests/util.py index 526ee54dd1..1d003c46e1 100644 --- a/cumulusci/tests/util.py +++ b/cumulusci/tests/util.py @@ -255,10 +255,13 @@ def _init_task(): task.bulk = FakeBulkAPI() task.sf = salesforce_client - with mock.patch( - "cumulusci.core.config.org_config.OrgConfig.is_person_accounts_enabled", - lambda: is_person_accounts_enabled, - ), mock.patch.object(task, "_init_task", _init_task): + with ( + mock.patch( + "cumulusci.core.config.org_config.OrgConfig.is_person_accounts_enabled", + lambda: is_person_accounts_enabled, + ), + mock.patch.object(task, "_init_task", _init_task), + ): yield @@ -290,8 +293,9 @@ def mock_env( **ENV_CLONE, } - with mock.patch("pathlib.Path.home", lambda: Path(home)), mock.patch.dict( - os.environ, new_environment, clear=True + with ( + mock.patch("pathlib.Path.home", lambda: Path(home)), + mock.patch.dict(os.environ, new_environment, clear=True), ): yield diff --git a/cumulusci/utils/__init__.py b/cumulusci/utils/__init__.py index 2d740c40cb..7d68f2cd35 100644 --- a/cumulusci/utils/__init__.py +++ b/cumulusci/utils/__init__.py @@ -430,7 +430,7 @@ def get_option_usage_string(name, option): """ usage_str = option.get("usage") if not usage_str: - usage_str = f"--{name} {name.replace('_','').upper()}" + usage_str = f"--{name} {name.replace('_', '').upper()}" return usage_str diff --git a/cumulusci/utils/http/tests/cassettes/ManualEditTestCompositeParallelSalesforce.test_http_headers.yaml b/cumulusci/utils/http/tests/cassettes/ManualEditTestCompositeParallelSalesforce.test_http_headers.yaml index 9971af16fc..cd8922435e 100644 --- a/cumulusci/utils/http/tests/cassettes/ManualEditTestCompositeParallelSalesforce.test_http_headers.yaml +++ b/cumulusci/utils/http/tests/cassettes/ManualEditTestCompositeParallelSalesforce.test_http_headers.yaml @@ -1,32 +1,33 @@ interactions: -- request: - body: '{"compositeRequest": [{"referenceId": "CCI__RefId__0__", "method": "GET", - "url": "/services/data/v49.0/sobjects", "httpHeaders": {"If-Modified-Since": - "Thu, 03 Sep 2020 21:35:07 GMT"}}, {"referenceId": "CCI__RefId__1__", "method": - "GET", "url": "/services/data/v49.0/sobjects", "httpHeaders": {"If-Modified-Since": - "Thu, 03 Sep 2020 21:35:07 GMT"}}, {"referenceId": "CCI__RefId__2__", "method": - "GET", "url": "/services/data/v49.0/sobjects", "httpHeaders": {"If-Modified-Since": - "Thu, 03 Sep 2020 21:35:07 GMT"}}]}' - headers: - Request-Headers: - - Elided - method: POST - uri: https://orgname.my.salesforce.com/services/data/v49.0/composite - response: - body: - string: "{\n \"compositeResponse\" : [ {\n \"body\" : null,\n \"httpHeaders\" - : {\n \"ETag\" : \"\\\"baf7f695\\\"\",\n \"Last-Modified\" : \"Fri, - 14 Aug 2020 20:53:02 GMT\"\n },\n \"httpStatusCode\" : 304,\n \"referenceId\" - : \"CCI__RefId__0__\"\n }, {\n \"body\" : null,\n \"httpHeaders\" : - {\n \"ETag\" : \"\\\"baf7f695\\\"\",\n \"Last-Modified\" : \"Fri, - 14 Aug 2020 20:53:02 GMT\"\n },\n \"httpStatusCode\" : 304,\n \"referenceId\" - : \"CCI__RefId__1__\"\n }, {\n \"body\" : null,\n \"httpHeaders\" : - {\n \"ETag\" : \"\\\"baf7f695\\\"\",\n \"Last-Modified\" : \"Fri, - 14 Aug 2020 20:53:02 GMT\"\n },\n \"httpStatusCode\" : 304,\n \"referenceId\" - : \"CCI__RefId__2__\"\n } ]\n}" - headers: - Response-Headers: Elided - status: - code: 200 - message: OK + - request: + body: '{"compositeRequest": [{"referenceId": "CCI__RefId__0__", "method": "GET", + "url": "/services/data/v49.0/sobjects", "httpHeaders": {"If-Modified-Since": + "Thu, 03 Sep 2020 21:35:07 GMT"}}, {"referenceId": "CCI__RefId__1__", "method": + "GET", "url": "/services/data/v49.0/sobjects", "httpHeaders": {"If-Modified-Since": + "Thu, 03 Sep 2020 21:35:07 GMT"}}, {"referenceId": "CCI__RefId__2__", "method": + "GET", "url": "/services/data/v49.0/sobjects", "httpHeaders": {"If-Modified-Since": + "Thu, 03 Sep 2020 21:35:07 GMT"}}]}' + headers: + Request-Headers: + - Elided + method: POST + uri: https://orgname.my.salesforce.com/services/data/v49.0/composite + response: + body: + string: + "{\n \"compositeResponse\" : [ {\n \"body\" : null,\n \"httpHeaders\" + : {\n \"ETag\" : \"\\\"baf7f695\\\"\",\n \"Last-Modified\" : \"Fri, + 14 Aug 2020 20:53:02 GMT\"\n },\n \"httpStatusCode\" : 304,\n \"referenceId\" + : \"CCI__RefId__0__\"\n }, {\n \"body\" : null,\n \"httpHeaders\" : + {\n \"ETag\" : \"\\\"baf7f695\\\"\",\n \"Last-Modified\" : \"Fri, + 14 Aug 2020 20:53:02 GMT\"\n },\n \"httpStatusCode\" : 304,\n \"referenceId\" + : \"CCI__RefId__1__\"\n }, {\n \"body\" : null,\n \"httpHeaders\" : + {\n \"ETag\" : \"\\\"baf7f695\\\"\",\n \"Last-Modified\" : \"Fri, + 14 Aug 2020 20:53:02 GMT\"\n },\n \"httpStatusCode\" : 304,\n \"referenceId\" + : \"CCI__RefId__2__\"\n } ]\n}" + headers: + Response-Headers: Elided + status: + code: 200 + message: OK version: 1 diff --git a/cumulusci/utils/parallel/task_worker_queues/parallel_worker.py b/cumulusci/utils/parallel/task_worker_queues/parallel_worker.py index 8130c37306..f8eeb86422 100644 --- a/cumulusci/utils/parallel/task_worker_queues/parallel_worker.py +++ b/cumulusci/utils/parallel/task_worker_queues/parallel_worker.py @@ -23,6 +23,7 @@ class SharedConfig(BaseModel): "Properties available both in the Queue and also each worker" + task_class: type project_config: BaseProjectConfig org_config: OrgConfig diff --git a/cumulusci/utils/parallel/task_worker_queues/parallel_worker_queue.py b/cumulusci/utils/parallel/task_worker_queues/parallel_worker_queue.py index ead2c0bfbf..b067cccab4 100644 --- a/cumulusci/utils/parallel/task_worker_queues/parallel_worker_queue.py +++ b/cumulusci/utils/parallel/task_worker_queues/parallel_worker_queue.py @@ -1,6 +1,6 @@ """Represents an inbox and configuration for a set of processes - that represent the same "step" in a pipeline. - """ +that represent the same "step" in a pipeline. +""" import logging import shutil diff --git a/cumulusci/utils/parallel/task_worker_queues/tests/test_parallel_worker.py b/cumulusci/utils/parallel/task_worker_queues/tests/test_parallel_worker.py index 6d49967ead..5df2207f47 100644 --- a/cumulusci/utils/parallel/task_worker_queues/tests/test_parallel_worker.py +++ b/cumulusci/utils/parallel/task_worker_queues/tests/test_parallel_worker.py @@ -174,21 +174,24 @@ def test_worker_queue(self, tmpdir): assert len(q.queued_job_dirs) == 3 def test_worker_queues_together(self, tmpdir): - with self.configure_worker_queue( - parent_dir=tmpdir, - name="start", - task_class=Sleep, - make_task_options=lambda *args, **kwargs: {"seconds": 0}, - queue_size=3, - num_workers=2, - ) as q1, self.configure_worker_queue( - parent_dir=tmpdir, - name="next", - task_class=Sleep, - make_task_options=lambda *args, **kwargs: {"seconds": 0}, - queue_size=3, - num_workers=2, - ) as q2: + with ( + self.configure_worker_queue( + parent_dir=tmpdir, + name="start", + task_class=Sleep, + make_task_options=lambda *args, **kwargs: {"seconds": 0}, + queue_size=3, + num_workers=2, + ) as q1, + self.configure_worker_queue( + parent_dir=tmpdir, + name="next", + task_class=Sleep, + make_task_options=lambda *args, **kwargs: {"seconds": 0}, + queue_size=3, + num_workers=2, + ) as q2, + ): q1.feeds_data_to(q2) q1.push(name="a") assert not q1.full @@ -220,21 +223,24 @@ def test_worker_queues_together(self, tmpdir): q2.tick() def test_worker_queues_together__outbox_cannot_be_removed(self, tmpdir): - with self.configure_worker_queue( - parent_dir=tmpdir, - name="start", - task_class=Sleep, - make_task_options=lambda *args, **kwargs: {"seconds": 0}, - queue_size=3, - num_workers=2, - ) as q1, self.configure_worker_queue( - parent_dir=tmpdir, - name="next", - task_class=Sleep, - make_task_options=lambda *args, **kwargs: {"seconds": 0}, - queue_size=3, - num_workers=2, - ) as q2: + with ( + self.configure_worker_queue( + parent_dir=tmpdir, + name="start", + task_class=Sleep, + make_task_options=lambda *args, **kwargs: {"seconds": 0}, + queue_size=3, + num_workers=2, + ) as q1, + self.configure_worker_queue( + parent_dir=tmpdir, + name="next", + task_class=Sleep, + make_task_options=lambda *args, **kwargs: {"seconds": 0}, + queue_size=3, + num_workers=2, + ) as q2, + ): q1.outbox_dir.rmdir() with mock.patch( "cumulusci.utils.parallel.task_worker_queues.parallel_worker_queue.logger.info" @@ -253,21 +259,24 @@ def make_task_options(path: Path, *args, **kwargs): assert path.parent.name == parentdirs.pop(0), path.parent.name return {"seconds": 0} - with self.configure_worker_queue( - parent_dir=tmpdir, - name="start", - task_class=Sleep, - make_task_options=make_task_options, - queue_size=3, - num_workers=2, - ) as q1, self.configure_worker_queue( - parent_dir=tmpdir, - name="next", - task_class=Sleep, - make_task_options=make_task_options, - queue_size=3, - num_workers=2, - ) as q2: + with ( + self.configure_worker_queue( + parent_dir=tmpdir, + name="start", + task_class=Sleep, + make_task_options=make_task_options, + queue_size=3, + num_workers=2, + ) as q1, + self.configure_worker_queue( + parent_dir=tmpdir, + name="next", + task_class=Sleep, + make_task_options=make_task_options, + queue_size=3, + num_workers=2, + ) as q2, + ): q1.feeds_data_to(q2) foo = tmpdir / "foo" foo.mkdir() @@ -296,7 +305,11 @@ def make_task_options(path: Path, *args, **kwargs): class TestParallelWorker: def test_terminate_parallel_worker(self): - with TemporaryDirectory() as failures_dir, TemporaryDirectory() as outbox_dir, TemporaryDirectory() as working_dir: + with ( + TemporaryDirectory() as failures_dir, + TemporaryDirectory() as outbox_dir, + TemporaryDirectory() as working_dir, + ): config = WorkerConfig( project_config=dummy_project_config, org_config=dummy_org_config, @@ -316,7 +329,11 @@ def test_terminate_parallel_worker(self): class TestTaskWorker: def test_worker__cannot_move_to_outdir(self): - with TemporaryDirectory() as failures_dir, TemporaryDirectory() as outbox_dir, TemporaryDirectory() as working_dir: + with ( + TemporaryDirectory() as failures_dir, + TemporaryDirectory() as outbox_dir, + TemporaryDirectory() as working_dir, + ): config = WorkerConfig( project_config=dummy_project_config, org_config=dummy_org_config, @@ -340,6 +357,7 @@ def test_worker__cannot_move_to_outdir(self): # tests, and we only use connected apps for persistent orgs, which # makes this even more messy. + # Also we usually mock refresh_oauth_token which is what would # invoke this. In other words, using integration tests to cover this # function is far easier than using unit test. diff --git a/cumulusci/utils/salesforce/count_sobjects.py b/cumulusci/utils/salesforce/count_sobjects.py index 216702ae4f..bccd518e05 100644 --- a/cumulusci/utils/salesforce/count_sobjects.py +++ b/cumulusci/utils/salesforce/count_sobjects.py @@ -1,6 +1,6 @@ import typing as T -from simple_salesforce import Salesforce +from simple_salesforce.api import Salesforce from cumulusci.utils.http.multi_request import CompositeParallelSalesforce from cumulusci.utils.iterators import partition diff --git a/cumulusci/utils/tests/test_org_schema.py b/cumulusci/utils/tests/test_org_schema.py index 34b664d606..5ef731cd60 100644 --- a/cumulusci/utils/tests/test_org_schema.py +++ b/cumulusci/utils/tests/test_org_schema.py @@ -152,17 +152,20 @@ def test_describe_to_sql(self, fallback_org_config): # This time it has nothing new to tell us so nothing # should be written to the local database except an updated # LastModifiedDate. - with mock_return_cached_responses(), patch( - "cumulusci.salesforce_api.org_schema.create_row" - ) as create_row, get_org_schema(FakeSF(), org_config) as schema: + with ( + mock_return_cached_responses(), + patch("cumulusci.salesforce_api.org_schema.create_row") as create_row, + get_org_schema(FakeSF(), org_config) as schema, + ): self.validate_schema_data(schema) for call in create_row.mock_calls: assert call[1][1].__name__ == "FileMetadata" def test_errors(self, org_config): - with mock_return_uncached_responses(self.cassette_data), get_org_schema( - FakeSF(), org_config - ) as schema: + with ( + mock_return_uncached_responses(self.cassette_data), + get_org_schema(FakeSF(), org_config) as schema, + ): with pytest.raises(KeyError): schema["Foo"] @@ -315,15 +318,18 @@ def throw_exception(*args, **kwargs): pass def test_minimal_schema(self, sf, org_config, vcr): - with vcr.use_cassette( - "ManualEditTestDescribeOrg.test_minimal_schema.yaml", - record_mode="none", - ), get_org_schema( - sf, - org_config, - included_objects=["Account", "Opportunity"], - force_recache=True, - ) as schema: + with ( + vcr.use_cassette( + "ManualEditTestDescribeOrg.test_minimal_schema.yaml", + record_mode="none", + ), + get_org_schema( + sf, + org_config, + included_objects=["Account", "Opportunity"], + force_recache=True, + ) as schema, + ): assert list(schema.keys()) == ["Account", "Opportunity"] def test_filter_by_name(self, sf, org_config): @@ -399,30 +405,42 @@ def test_error_populate_without_include_counts(self, sf, org_config): def test_filter_by_populated(self, sf, org_config): with mock_return_uncached_responses(self.cassette_data): - with patch( - "cumulusci.salesforce_api.org_schema.count_sobjects", - lambda *args: ( - {"Account": 10, "Contact": 5, "PermissionSet": 0}, - [], - [], + with ( + patch( + "cumulusci.salesforce_api.org_schema.count_sobjects", + lambda *args: ( + {"Account": 10, "Contact": 5, "PermissionSet": 0}, + [], + [], + ), ), - ), get_org_schema( - FakeSF(), org_config, include_counts=True, filters=[Filters.populated] - ) as schema: + get_org_schema( + FakeSF(), + org_config, + include_counts=True, + filters=[Filters.populated], + ) as schema, + ): assert "Account" in schema assert "PermissionSet" not in schema def test_error_while_counting(self, sf, org_config, caplog): with mock_return_uncached_responses(self.cassette_data): - with patch( - "cumulusci.salesforce_api.org_schema.count_sobjects", - lambda *args: ( - {"Account": 10, "Contact": 5, "PermissionSet": 0}, - [], - [HTTPRequestError("Error! Apostasy!", None)] * 15, + with ( + patch( + "cumulusci.salesforce_api.org_schema.count_sobjects", + lambda *args: ( + {"Account": 10, "Contact": 5, "PermissionSet": 0}, + [], + [HTTPRequestError("Error! Apostasy!", None)] * 15, + ), + ), + get_org_schema( + FakeSF(), + org_config, + include_counts=True, + filters=[Filters.populated], ), - ), get_org_schema( - FakeSF(), org_config, include_counts=True, filters=[Filters.populated] ): pass assert "Apostasy" in caplog.text @@ -430,11 +448,17 @@ def test_error_while_counting(self, sf, org_config, caplog): def test_old_schema_version(self, sf, org_config, caplog): with mock_return_uncached_responses(self.cassette_data): - with patch( - "cumulusci.salesforce_api.org_schema.Schema.CurrentFormatVersion", 7 - ), get_org_schema( - FakeSF(), org_config, include_counts=True, filters=[Filters.populated] - ) as schema: + with ( + patch( + "cumulusci.salesforce_api.org_schema.Schema.CurrentFormatVersion", 7 + ), + get_org_schema( + FakeSF(), + org_config, + include_counts=True, + filters=[Filters.populated], + ) as schema, + ): assert schema.version == 7 class FakeSilentMigration(Exception): @@ -443,14 +467,21 @@ class FakeSilentMigration(Exception): def __init__(self, *args, **kwargs): self.__class__.called = True - with patch( - "cumulusci.salesforce_api.org_schema.SilentMigration", - FakeSilentMigration, - ), patch( - "cumulusci.salesforce_api.org_schema.Schema.CurrentFormatVersion", 8 - ), get_org_schema( - FakeSF(), org_config, include_counts=True, filters=[Filters.populated] - ) as schema: + with ( + patch( + "cumulusci.salesforce_api.org_schema.SilentMigration", + FakeSilentMigration, + ), + patch( + "cumulusci.salesforce_api.org_schema.Schema.CurrentFormatVersion", 8 + ), + get_org_schema( + FakeSF(), + org_config, + include_counts=True, + filters=[Filters.populated], + ) as schema, + ): assert schema.version == 8 assert FakeSilentMigration.called @@ -471,9 +502,9 @@ def test_schema_populated_real(self, sf, org_config, ensure_records): ) as schema: assert "Account" in schema, schema.keys() assert "Case" not in schema, schema.keys() # not in included_objects - assert ( - "Opportunity" not in schema - ), schema.keys() # because not populated + assert "Opportunity" not in schema, ( + schema.keys() + ) # because not populated # Create one case and ensure it is noticed. with ensure_records({"Case": [{}]}): @@ -482,9 +513,9 @@ def test_schema_populated_real(self, sf, org_config, ensure_records): ) as schema: assert "Account" in schema, schema.keys() assert "Case" in schema, schema.keys() - assert ( - "Opportunity" not in schema - ), schema.keys() # because not populated + assert "Opportunity" not in schema, ( + schema.keys() + ) # because not populated @pytest.mark.needs_org() # too hard to make these VCR-compatible due to data volume diff --git a/cumulusci/utils/version_strings.py b/cumulusci/utils/version_strings.py index a216cd78e1..3a8c7318b3 100644 --- a/cumulusci/utils/version_strings.py +++ b/cumulusci/utils/version_strings.py @@ -144,7 +144,6 @@ def __ge__(self, other): class StrictVersion(Version): - """Version numbering for anal retentives and software idealists. Implements the standard interface for version number classes as described above. A version number consists of two or three @@ -319,7 +318,6 @@ def _cmp(self, other): class LooseVersion(Version): - """Version numbering for anarchists and software realists. Implements the standard interface for version number classes as described above. A version number consists of a series of numbers, diff --git a/cumulusci/utils/xml/salesforce_encoding.py b/cumulusci/utils/xml/salesforce_encoding.py index 5d4cde09d0..15ed5e3acd 100644 --- a/cumulusci/utils/xml/salesforce_encoding.py +++ b/cumulusci/utils/xml/salesforce_encoding.py @@ -16,9 +16,9 @@ def serialize_xml_for_salesforce( else: root = element_or_tree - assert hasattr( - root, "nsmap" - ), "Passed object should be lxml.etree._ElementTree or lxml.etree._ElementTree" + assert hasattr(root, "nsmap"), ( + "Passed object should be lxml.etree._ElementTree or lxml.etree._ElementTree" + ) if include_parent_namespaces: new_namespace_declarations = {prefix: url for prefix, url in root.nsmap.items()} diff --git a/cumulusci/utils/yaml/cumulusci_yml.py b/cumulusci/utils/yaml/cumulusci_yml.py index 78cc9ac960..bf1b9352e7 100644 --- a/cumulusci/utils/yaml/cumulusci_yml.py +++ b/cumulusci/utils/yaml/cumulusci_yml.py @@ -53,9 +53,9 @@ class Step(CCIDictModel): def _check(cls, values): has_task = values.get("task") and values["task"] != "None" has_flow = values.get("flow") and values["flow"] != "None" - assert not ( - has_task and has_flow - ), "Steps must have either task or flow but not both" + assert not (has_task and has_flow), ( + "Steps must have either task or flow but not both" + ) return values @@ -273,6 +273,7 @@ def validate_data( class ErrorDict(TypedDict): "The structure of a Pydantic error dictionary. Google TypedDict if its new to you." + loc: Sequence[Union[str, int]] msg: str type: str @@ -292,9 +293,9 @@ def cci_safe_load( ) -> dict: """Load a CumulusCI.yml file and issue warnings for unknown structures.""" errors = [] - assert not ( - on_error and logger - ), "Please specify either on_error or logger but not both" + assert not (on_error and logger), ( + "Please specify either on_error or logger but not both" + ) on_error = on_error or errors.append logger = logger or default_logger diff --git a/cumulusci/utils/yaml/safer_loader.py b/cumulusci/utils/yaml/safer_loader.py index 55c7433b94..844e0f61b7 100644 --- a/cumulusci/utils/yaml/safer_loader.py +++ b/cumulusci/utils/yaml/safer_loader.py @@ -10,7 +10,7 @@ from cumulusci.core.exceptions import YAMLParseException from cumulusci.utils.fileutils import FSResource, load_from_source -NBSP = "\u00A0" +NBSP = "\u00a0" pattern = re.compile(r"^\s*[\u00A0]+\s*", re.MULTILINE) diff --git a/cumulusci/utils/yaml/tests/test_safer_loader.py b/cumulusci/utils/yaml/tests/test_safer_loader.py index 5bd499842e..d1b7fb23b0 100644 --- a/cumulusci/utils/yaml/tests/test_safer_loader.py +++ b/cumulusci/utils/yaml/tests/test_safer_loader.py @@ -28,7 +28,7 @@ def test_simple_load(caplog): def test_convert_nbsp(caplog): yaml = """xyz: - \u00A0 y: abc""" + \u00a0 y: abc""" cciyml = load_yaml_data(StringIO(yaml)) assert "space character" in caplog.text @@ -38,7 +38,7 @@ def test_convert_nbsp(caplog): def test_converter(): inp = """xyz: - \u00A0 y: abc""" + \u00a0 y: abc""" outp = """xyz: y: abc""" @@ -48,7 +48,7 @@ def test_converter(): def test_converter_is_selective(): inp = """xyz: - y: abc\u00A0""" + y: abc\u00a0""" rc = _replace_nbsp(inp, "filename") assert rc == inp diff --git a/docs/about.md b/docs/about.md index 21d3eb7866..6ad6a3d7a7 100644 --- a/docs/about.md +++ b/docs/about.md @@ -5,4 +5,4 @@ contributing history -``` \ No newline at end of file +``` diff --git a/docs/conf.py b/docs/conf.py index 208a287b8d..a1f8add442 100755 --- a/docs/conf.py +++ b/docs/conf.py @@ -45,7 +45,7 @@ "sphinx.ext.autodoc", "sphinx.ext.viewcode", "sphinx.ext.autosectionlabel", - "myst_parser" # , + "myst_parser", # , # "recommonmark" ] @@ -237,8 +237,8 @@ latex_elements = { - 'preamble': r'''\renewcommand{\familydefault}{\sfdefault} % Set default font to sans-serif -''', + "preamble": r"""\renewcommand{\familydefault}{\sfdefault} % Set default font to sans-serif +""", } diff --git a/docs/config.md b/docs/config.md index fc9ebd6921..b090fab81e 100644 --- a/docs/config.md +++ b/docs/config.md @@ -86,7 +86,9 @@ Options Require at least X percent code coverage across the org following the test run. Default: 90 ``` + (add-a-custom-task)= + ### Add a Custom Task To define a new task for your project, add the task name under the diff --git a/docs/cumulusci-flow.md b/docs/cumulusci-flow.md index 8dfaaf51f3..75ee3e5c97 100644 --- a/docs/cumulusci-flow.md +++ b/docs/cumulusci-flow.md @@ -296,6 +296,7 @@ child branches. This means that despite not receive automerges until the parent branch tests successfully. (release-to-future-release-merges)= + ### Release to (Future) Release Merges Because release branches are so long-lived, and so much work goes into diff --git a/docs/data.md b/docs/data.md index 614dc0da78..6ce6f15980 100644 --- a/docs/data.md +++ b/docs/data.md @@ -816,13 +816,15 @@ Extract the data for a dataset from an org and persist it to disk. `mapping` and either `sql_path` or `database_url` must be supplied. -Example: +Example: + ```console cci task run extract_dataset -o mapping datasets/qa/mapping.yml -o sql_path datasets/qa/data.sql --org qa ``` (data-load-dataset)= -### `load_dataset` + +### `load_dataset` Load the data for a dataset into an org. If the storage is a database, persist new Salesforce Ids to storage. @@ -842,7 +844,7 @@ persist new Salesforce Ids to storage. `mapping` and either `sql_path` or `database_url` must be supplied. -Example: +Example: ```console cci task run load_dataset -o mapping datasets/qa/mapping.yml -o sql_path datasets/qa/data.sql --org qa @@ -912,7 +914,7 @@ after their target records become available. - `namespace_prefix`: The namespace prefix to treat as belonging to the project, if any -Example: +Example: ```console cci task run generate_dataset_mapping --org qa -o namespace_prefix my_ns diff --git a/docs/deploy.md b/docs/deploy.md index fb5140a768..f20ec9ad9a 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -1,4 +1,5 @@ (configure-metadata-deployment)= + # Configure Metadata Deployment CumulusCI's `deploy` task uses the Metadata API to deploy metadata from the repository to a Salesforce org. `deploy` offers multiple sophisticated capabilities to suit the needs of your project. diff --git a/docs/env-var-reference.md b/docs/env-var-reference.md index 5f17768c57..8aeb51ceb4 100644 --- a/docs/env-var-reference.md +++ b/docs/env-var-reference.md @@ -27,6 +27,7 @@ Used for specifying a GitHub Repository for CumulusCI to use when running in a CI environment. (cumulusci-system-certs)= + ## `CUMULUSCI_SYSTEM_CERTS` If set to `True`, CumulusCI will configure the Python `requests` library @@ -44,6 +45,7 @@ Contents of a JSON Web Token (JWT) used to [authenticate a GitHub app](https://developer.github.com/apps/building-github-apps/authenticating-with-github-apps/##authenticating-as-a-github-app). (github-token)= + ## `GITHUB_TOKEN` A GitHub [personal access diff --git a/docs/get-started.md b/docs/get-started.md index 3245e8b39f..77d0ee0726 100644 --- a/docs/get-started.md +++ b/docs/get-started.md @@ -123,6 +123,7 @@ pipx install cumulusci When finished, [verify your installation](verify-your-installation). (update-environment-variables-manually)= + #### Update Environment Variables Manually 1. Click Start and search for `edit environment variables`, or open diff --git a/docs/headless.md b/docs/headless.md index d23afb012a..a404f3d361 100644 --- a/docs/headless.md +++ b/docs/headless.md @@ -86,7 +86,11 @@ then the attributes: `callback_url`, `client_id`, and `client_secret` would need to be provided in the following format: ```json -{"callback_url": "", "client_id": "", "client_secret": ""} +{ + "callback_url": "", + "client_id": "", + "client_secret": "" +} ``` > The values ``, ``, and `` diff --git a/docs/robot/Keywords.html b/docs/robot/Keywords.html index 2503012352..8fbe05109c 100644 --- a/docs/robot/Keywords.html +++ b/docs/robot/Keywords.html @@ -1,4 +1,4 @@ - + diff --git a/docs/unpackaged.md b/docs/unpackaged.md index a8061d860d..99383edb57 100644 --- a/docs/unpackaged.md +++ b/docs/unpackaged.md @@ -366,8 +366,6 @@ config_regression: For more details on customizing tasks and flows, see the [](config) section. - - ```{toctree} --- maxdepth: 1 diff --git a/pyproject.toml b/pyproject.toml index 638ad7bb50..20ae38b831 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,6 +67,7 @@ dev = [ "factory-boy>=3.3.1", "furo>=2023.3.27", "jsonschema>=4.23.0", + "ruff>=0.15.8", "pytest>=7.0.1", "pytest-cov>=5.0.0", "pytest-random-order>=1.1.1", @@ -78,9 +79,7 @@ dev = [ "vcrpy>=6.0.2", ] lint = [ - "black>=24.8.0", - "flake8<4", - "isort>=5.13.2", + "ruff>=0.15.8", "pre-commit>=3.5.0", ] @@ -117,7 +116,6 @@ include = [ [tool.hatch.build.targets.sdist] include = [ "/cumulusci", - "/requirements/*", # Needed by tox "README.md", # needed by hatch-fancy-pypi-readme "docs/history.md", # ditto @@ -140,9 +138,6 @@ end-before = "\n\n" ####################### # Tool configurations # ####################### -[tool.black] -exclude = '^/(\.|dist|pybuild|venv)' - [tool.coverage.run] omit = ["*/tests/*", "cumulusci/files/*"] source = ["cumulusci"] @@ -164,12 +159,23 @@ filterwarnings = [ "ignore::SyntaxWarning:.*.selenium", ] -[tool.isort] -profile = "black" -multi_line_output = 3 -skip_glob = "cumulusci/**/__init__.py" -known_first_party = "cumulusci" -known_third_party = "robot" +[tool.ruff] +target-version = "py311" +exclude = [".git", "dist", "pybuild", "venv", ".tox", ".eggs"] + +[tool.ruff.lint] +# Match the historical .flake8 `select` that was checked in CI. The file listed +# B, B9, and T4, but flake8-bugbear / flake8-print were not installed, so those +# codes were effectively no-ops. Enforce the subset that actually ran: E, F, W, C. +select = ["E", "F", "W", "C90"] +ignore = ["E203", "E266", "E402", "E501", "C901"] + +[tool.ruff.lint.isort] +known-first-party = ["cumulusci"] +known-third-party = ["robot"] + +[tool.ruff.format] +skip-magic-trailing-comma = false [tool.pyright] reportMissingImports = "none" diff --git a/tox.ini b/tox.ini deleted file mode 100644 index e86726dfea..0000000000 --- a/tox.ini +++ /dev/null @@ -1,13 +0,0 @@ -[tox] -envlist = py38, py39, py310 - -[testenv] -setenv = - PYTHONPATH = {toxinidir}:{toxinidir}/cumulusci - -commands = coverage run {envbindir}/pytest [] - -deps = - -r{toxinidir}/requirements_dev.txt -install_command = - python -m pip install {opts} {packages} diff --git a/utility/update-history.py b/utility/update-history.py index 028a279ff3..d94f205e7c 100644 --- a/utility/update-history.py +++ b/utility/update-history.py @@ -1,6 +1,7 @@ """ Update the history file. """ + from pathlib import Path START_MARKER = "" diff --git a/uv.lock b/uv.lock index 40ab86b82c..fafe86b303 100644 --- a/uv.lock +++ b/uv.lock @@ -75,38 +75,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b", size = 187285, upload-time = "2025-04-15T17:05:12.221Z" }, ] -[[package]] -name = "black" -version = "26.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "mypy-extensions" }, - { name = "packaging" }, - { name = "pathspec" }, - { name = "platformdirs" }, - { name = "pytokens" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e1/c5/61175d618685d42b005847464b8fb4743a67b1b8fdb75e50e5a96c31a27a/black-26.3.1.tar.gz", hash = "sha256:2c50f5063a9641c7eed7795014ba37b0f5fa227f3d408b968936e24bc0566b07", size = 666155, upload-time = "2026-03-12T03:36:03.593Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/17/57/5f11c92861f9c92eb9dddf515530bc2d06db843e44bdcf1c83c1427824bc/black-26.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:28ef38aee69e4b12fda8dba75e21f9b4f979b490c8ac0baa7cb505369ac9e1ff", size = 1851987, upload-time = "2026-03-12T03:40:06.248Z" }, - { url = "https://files.pythonhosted.org/packages/54/aa/340a1463660bf6831f9e39646bf774086dbd8ca7fc3cded9d59bbdf4ad0a/black-26.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf9bf162ed91a26f1adba8efda0b573bc6924ec1408a52cc6f82cb73ec2b142c", size = 1689499, upload-time = "2026-03-12T03:40:07.642Z" }, - { url = "https://files.pythonhosted.org/packages/f3/01/b726c93d717d72733da031d2de10b92c9fa4c8d0c67e8a8a372076579279/black-26.3.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:474c27574d6d7037c1bc875a81d9be0a9a4f9ee95e62800dab3cfaadbf75acd5", size = 1754369, upload-time = "2026-03-12T03:40:09.279Z" }, - { url = "https://files.pythonhosted.org/packages/e3/09/61e91881ca291f150cfc9eb7ba19473c2e59df28859a11a88248b5cbbc4d/black-26.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:5e9d0d86df21f2e1677cc4bd090cd0e446278bcbbe49bf3659c308c3e402843e", size = 1413613, upload-time = "2026-03-12T03:40:10.943Z" }, - { url = "https://files.pythonhosted.org/packages/16/73/544f23891b22e7efe4d8f812371ab85b57f6a01b2fc45e3ba2e52ba985b8/black-26.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:9a5e9f45e5d5e1c5b5c29b3bd4265dcc90e8b92cf4534520896ed77f791f4da5", size = 1219719, upload-time = "2026-03-12T03:40:12.597Z" }, - { url = "https://files.pythonhosted.org/packages/dc/f8/da5eae4fc75e78e6dceb60624e1b9662ab00d6b452996046dfa9b8a6025b/black-26.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e6f89631eb88a7302d416594a32faeee9fb8fb848290da9d0a5f2903519fc1", size = 1895920, upload-time = "2026-03-12T03:40:13.921Z" }, - { url = "https://files.pythonhosted.org/packages/2c/9f/04e6f26534da2e1629b2b48255c264cabf5eedc5141d04516d9d68a24111/black-26.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41cd2012d35b47d589cb8a16faf8a32ef7a336f56356babd9fcf70939ad1897f", size = 1718499, upload-time = "2026-03-12T03:40:15.239Z" }, - { url = "https://files.pythonhosted.org/packages/04/91/a5935b2a63e31b331060c4a9fdb5a6c725840858c599032a6f3aac94055f/black-26.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f76ff19ec5297dd8e66eb64deda23631e642c9393ab592826fd4bdc97a4bce7", size = 1794994, upload-time = "2026-03-12T03:40:17.124Z" }, - { url = "https://files.pythonhosted.org/packages/e7/0a/86e462cdd311a3c2a8ece708d22aba17d0b2a0d5348ca34b40cdcbea512e/black-26.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:ddb113db38838eb9f043623ba274cfaf7d51d5b0c22ecb30afe58b1bb8322983", size = 1420867, upload-time = "2026-03-12T03:40:18.83Z" }, - { url = "https://files.pythonhosted.org/packages/5b/e5/22515a19cb7eaee3440325a6b0d95d2c0e88dd180cb011b12ae488e031d1/black-26.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:dfdd51fc3e64ea4f35873d1b3fb25326773d55d2329ff8449139ebaad7357efb", size = 1230124, upload-time = "2026-03-12T03:40:20.425Z" }, - { url = "https://files.pythonhosted.org/packages/f5/77/5728052a3c0450c53d9bb3945c4c46b91baa62b2cafab6801411b6271e45/black-26.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:855822d90f884905362f602880ed8b5df1b7e3ee7d0db2502d4388a954cc8c54", size = 1895034, upload-time = "2026-03-12T03:40:21.813Z" }, - { url = "https://files.pythonhosted.org/packages/52/73/7cae55fdfdfbe9d19e9a8d25d145018965fe2079fa908101c3733b0c55a0/black-26.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8a33d657f3276328ce00e4d37fe70361e1ec7614da5d7b6e78de5426cb56332f", size = 1718503, upload-time = "2026-03-12T03:40:23.666Z" }, - { url = "https://files.pythonhosted.org/packages/e1/87/af89ad449e8254fdbc74654e6467e3c9381b61472cc532ee350d28cfdafb/black-26.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f1cd08e99d2f9317292a311dfe578fd2a24b15dbce97792f9c4d752275c1fa56", size = 1793557, upload-time = "2026-03-12T03:40:25.497Z" }, - { url = "https://files.pythonhosted.org/packages/43/10/d6c06a791d8124b843bf325ab4ac7d2f5b98731dff84d6064eafd687ded1/black-26.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:c7e72339f841b5a237ff14f7d3880ddd0fc7f98a1199e8c4327f9a4f478c1839", size = 1422766, upload-time = "2026-03-12T03:40:27.14Z" }, - { url = "https://files.pythonhosted.org/packages/59/4f/40a582c015f2d841ac24fed6390bd68f0fc896069ff3a886317959c9daf8/black-26.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc622538b430aa4c8c853f7f63bc582b3b8030fd8c80b70fb5fa5b834e575c2", size = 1232140, upload-time = "2026-03-12T03:40:28.882Z" }, - { url = "https://files.pythonhosted.org/packages/8e/0d/52d98722666d6fc6c3dd4c76df339501d6efd40e0ff95e6186a7b7f0befd/black-26.3.1-py3-none-any.whl", hash = "sha256:2bd5aa94fc267d38bb21a70d7410a89f1a1d318841855f698746f8e7f51acd1b", size = 207542, upload-time = "2026-03-12T03:36:01.668Z" }, -] - [[package]] name = "cachetools" version = "6.1.0" @@ -130,7 +98,7 @@ name = "cffi" version = "2.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pycparser", marker = "implementation_name != 'PyPy'" }, + { name = "pycparser", marker = "implementation_name != 'PyPy' and platform_python_implementation != 'PyPy'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } wheels = [ @@ -420,6 +388,7 @@ dev = [ { name = "pytest-random-order" }, { name = "pytest-vcr" }, { name = "responses" }, + { name = "ruff" }, { name = "testfixtures" }, { name = "tox" }, { name = "typeguard" }, @@ -430,10 +399,8 @@ docs = [ { name = "sphinx" }, ] lint = [ - { name = "black" }, - { name = "flake8" }, - { name = "isort" }, { name = "pre-commit" }, + { name = "ruff" }, ] [package.metadata] @@ -488,6 +455,7 @@ dev = [ { name = "pytest-random-order", specifier = ">=1.1.1" }, { name = "pytest-vcr", specifier = ">=1.0.2" }, { name = "responses", specifier = ">=0.23.1" }, + { name = "ruff", specifier = ">=0.15.8" }, { name = "testfixtures", specifier = ">=8.3.0" }, { name = "tox", specifier = ">=4.20.0" }, { name = "typeguard", specifier = "<=2.13.3" }, @@ -498,10 +466,8 @@ docs = [ { name = "sphinx", specifier = ">=5.3.0" }, ] lint = [ - { name = "black", specifier = ">=24.8.0" }, - { name = "flake8", specifier = "<4" }, - { name = "isort", specifier = ">=5.13.2" }, { name = "pre-commit", specifier = ">=3.5.0" }, + { name = "ruff", specifier = ">=0.15.8" }, ] [[package]] @@ -588,20 +554,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl", hash = "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70", size = 26759, upload-time = "2026-03-11T20:45:37.437Z" }, ] -[[package]] -name = "flake8" -version = "3.9.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mccabe" }, - { name = "pycodestyle" }, - { name = "pyflakes" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9e/47/15b267dfe7e03dca4c4c06e7eadbd55ef4dfd368b13a0bab36d708b14366/flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b", size = 164777, upload-time = "2021-05-08T19:52:34.369Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/80/35a0716e5d5101e643404dabd20f07f5528a21f3ef4032d31a49c913237b/flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907", size = 73147, upload-time = "2021-05-08T19:52:32.476Z" }, -] - [[package]] name = "furo" version = "2025.7.19" @@ -731,15 +683,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" }, ] -[[package]] -name = "isort" -version = "6.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b8/21/1e2a441f74a653a144224d7d21afe8f4169e6c7c20bb13aec3a2dc3815e0/isort-6.0.1.tar.gz", hash = "sha256:1cb5df28dfbc742e490c5e41bad6da41b805b0a8be7bc93cd0fb2a8a890ac450", size = 821955, upload-time = "2025-02-26T21:13:16.955Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/11/114d0a5f4dabbdcedc1125dee0888514c3c3b16d3e9facad87ed96fad97c/isort-6.0.1-py3-none-any.whl", hash = "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615", size = 94186, upload-time = "2025-02-26T21:13:14.911Z" }, -] - [[package]] name = "jeepney" version = "0.9.0" @@ -926,15 +869,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, ] -[[package]] -name = "mccabe" -version = "0.6.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/06/18/fa675aa501e11d6d6ca0ae73a101b2f3571a565e0f7d38e062eec18a91ee/mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f", size = 8612, upload-time = "2017-01-26T22:13:15.699Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/87/89/479dc97e18549e21354893e4ee4ef36db1d237534982482c3681ee6e7b57/mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", size = 8556, upload-time = "2017-01-26T22:13:14.36Z" }, -] - [[package]] name = "mdit-py-plugins" version = "0.4.2" @@ -1046,15 +980,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d8/30/9aec301e9772b098c1f5c0ca0279237c9766d94b97802e9888010c64b0ed/multidict-6.6.3-py3-none-any.whl", hash = "sha256:8db10f29c7541fc5da4defd8cd697e1ca429db743fa716325f236079b96f775a", size = 12313, upload-time = "2025-06-30T15:53:45.437Z" }, ] -[[package]] -name = "mypy-extensions" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, -] - [[package]] name = "myst-parser" version = "4.0.1" @@ -1199,15 +1124,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d5/f9/07086f5b0f2a19872554abeea7658200824f5835c58a106fa8f2ae96a46c/pandas-2.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5db9637dbc24b631ff3707269ae4559bce4b7fd75c1c4d7e13f40edc42df4444", size = 13189044, upload-time = "2025-07-07T19:19:39.999Z" }, ] -[[package]] -name = "pathspec" -version = "1.0.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, -] - [[package]] name = "platformdirs" version = "4.3.8" @@ -1330,15 +1246,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885, upload-time = "2025-02-13T21:54:37.486Z" }, ] -[[package]] -name = "pycodestyle" -version = "2.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/02/b3/c832123f2699892c715fcdfebb1a8fdeffa11bb7b2350e46ecdd76b45a20/pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef", size = 103640, upload-time = "2021-03-14T18:44:04.177Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/de/cc/227251b1471f129bc35e966bb0fceb005969023926d744139642d847b7ae/pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068", size = 41725, upload-time = "2021-03-14T18:44:02.097Z" }, -] - [[package]] name = "pycparser" version = "2.22" @@ -1428,15 +1335,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757, upload-time = "2025-04-23T18:33:30.645Z" }, ] -[[package]] -name = "pyflakes" -version = "2.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a8/0f/0dc480da9162749bf629dca76570972dd9cce5bedc60196a3c912875c87d/pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db", size = 68567, upload-time = "2021-03-24T16:32:56.157Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6c/11/2a745612f1d3cbbd9c69ba14b1b43a35a2f5c3c81cd0124508c52c64307f/pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3", size = 68805, upload-time = "2021-03-24T16:32:54.562Z" }, -] - [[package]] name = "pygments" version = "2.20.0" @@ -1558,30 +1456,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d8/db/795879cc3ddfe338599bddea6388cc5100b088db0a4caf6e6c1af1c27e04/python_discovery-1.2.2-py3-none-any.whl", hash = "sha256:e1ae95d9af875e78f15e19aed0c6137ab1bb49c200f21f5061786490c9585c7a", size = 31894, upload-time = "2026-04-07T17:28:48.09Z" }, ] -[[package]] -name = "pytokens" -version = "0.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/92/790ebe03f07b57e53b10884c329b9a1a308648fc083a6d4a39a10a28c8fc/pytokens-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d70e77c55ae8380c91c0c18dea05951482e263982911fc7410b1ffd1dadd3440", size = 160864, upload-time = "2026-01-30T01:02:57.882Z" }, - { url = "https://files.pythonhosted.org/packages/13/25/a4f555281d975bfdd1eba731450e2fe3a95870274da73fb12c40aeae7625/pytokens-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a58d057208cb9075c144950d789511220b07636dd2e4708d5645d24de666bdc", size = 248565, upload-time = "2026-01-30T01:02:59.912Z" }, - { url = "https://files.pythonhosted.org/packages/17/50/bc0394b4ad5b1601be22fa43652173d47e4c9efbf0044c62e9a59b747c56/pytokens-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b49750419d300e2b5a3813cf229d4e5a4c728dae470bcc89867a9ad6f25a722d", size = 260824, upload-time = "2026-01-30T01:03:01.471Z" }, - { url = "https://files.pythonhosted.org/packages/4e/54/3e04f9d92a4be4fc6c80016bc396b923d2a6933ae94b5f557c939c460ee0/pytokens-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9907d61f15bf7261d7e775bd5d7ee4d2930e04424bab1972591918497623a16", size = 264075, upload-time = "2026-01-30T01:03:04.143Z" }, - { url = "https://files.pythonhosted.org/packages/d1/1b/44b0326cb5470a4375f37988aea5d61b5cc52407143303015ebee94abfd6/pytokens-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:ee44d0f85b803321710f9239f335aafe16553b39106384cef8e6de40cb4ef2f6", size = 103323, upload-time = "2026-01-30T01:03:05.412Z" }, - { url = "https://files.pythonhosted.org/packages/41/5d/e44573011401fb82e9d51e97f1290ceb377800fb4eed650b96f4753b499c/pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083", size = 160663, upload-time = "2026-01-30T01:03:06.473Z" }, - { url = "https://files.pythonhosted.org/packages/f0/e6/5bbc3019f8e6f21d09c41f8b8654536117e5e211a85d89212d59cbdab381/pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1", size = 255626, upload-time = "2026-01-30T01:03:08.177Z" }, - { url = "https://files.pythonhosted.org/packages/bf/3c/2d5297d82286f6f3d92770289fd439956b201c0a4fc7e72efb9b2293758e/pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1", size = 269779, upload-time = "2026-01-30T01:03:09.756Z" }, - { url = "https://files.pythonhosted.org/packages/20/01/7436e9ad693cebda0551203e0bf28f7669976c60ad07d6402098208476de/pytokens-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9", size = 268076, upload-time = "2026-01-30T01:03:10.957Z" }, - { url = "https://files.pythonhosted.org/packages/2e/df/533c82a3c752ba13ae7ef238b7f8cdd272cf1475f03c63ac6cf3fcfb00b6/pytokens-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68", size = 103552, upload-time = "2026-01-30T01:03:12.066Z" }, - { url = "https://files.pythonhosted.org/packages/cb/dc/08b1a080372afda3cceb4f3c0a7ba2bde9d6a5241f1edb02a22a019ee147/pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b", size = 160720, upload-time = "2026-01-30T01:03:13.843Z" }, - { url = "https://files.pythonhosted.org/packages/64/0c/41ea22205da480837a700e395507e6a24425151dfb7ead73343d6e2d7ffe/pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f", size = 254204, upload-time = "2026-01-30T01:03:14.886Z" }, - { url = "https://files.pythonhosted.org/packages/e0/d2/afe5c7f8607018beb99971489dbb846508f1b8f351fcefc225fcf4b2adc0/pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1", size = 268423, upload-time = "2026-01-30T01:03:15.936Z" }, - { url = "https://files.pythonhosted.org/packages/68/d4/00ffdbd370410c04e9591da9220a68dc1693ef7499173eb3e30d06e05ed1/pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4", size = 266859, upload-time = "2026-01-30T01:03:17.458Z" }, - { url = "https://files.pythonhosted.org/packages/a7/c9/c3161313b4ca0c601eeefabd3d3b576edaa9afdefd32da97210700e47652/pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78", size = 103520, upload-time = "2026-01-30T01:03:18.652Z" }, - { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" }, -] - [[package]] name = "pytz" version = "2025.2" @@ -1890,6 +1764,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/46/cb/fdb216f2b8bbec9c43655a79f2f280b2ba7822b2c8396ecceafa0c232320/rst2ansi-0.1.5-py3-none-any.whl", hash = "sha256:b2cf192e38975918d07540bba7d673550cd7d28ca7443410984e22d5ab058fb3", size = 18414, upload-time = "2016-05-24T16:43:27.677Z" }, ] +[[package]] +name = "ruff" +version = "0.15.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/d9/aa3f7d59a10ef6b14fe3431706f854dbf03c5976be614a9796d36326810c/ruff-0.15.10.tar.gz", hash = "sha256:d1f86e67ebfdef88e00faefa1552b5e510e1d35f3be7d423dc7e84e63788c94e", size = 4631728, upload-time = "2026-04-09T14:06:09.884Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/00/a1c2fdc9939b2c03691edbda290afcd297f1f389196172826b03d6b6a595/ruff-0.15.10-py3-none-linux_armv6l.whl", hash = "sha256:0744e31482f8f7d0d10a11fcbf897af272fefdfcb10f5af907b18c2813ff4d5f", size = 10563362, upload-time = "2026-04-09T14:06:21.189Z" }, + { url = "https://files.pythonhosted.org/packages/5c/15/006990029aea0bebe9d33c73c3e28c80c391ebdba408d1b08496f00d422d/ruff-0.15.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b1e7c16ea0ff5a53b7c2df52d947e685973049be1cdfe2b59a9c43601897b22e", size = 10951122, upload-time = "2026-04-09T14:06:02.236Z" }, + { url = "https://files.pythonhosted.org/packages/f2/c0/4ac978fe874d0618c7da647862afe697b281c2806f13ce904ad652fa87e4/ruff-0.15.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:93cc06a19e5155b4441dd72808fdf84290d84ad8a39ca3b0f994363ade4cebb1", size = 10314005, upload-time = "2026-04-09T14:06:00.026Z" }, + { url = "https://files.pythonhosted.org/packages/da/73/c209138a5c98c0d321266372fc4e33ad43d506d7e5dd817dd89b60a8548f/ruff-0.15.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83e1dd04312997c99ea6965df66a14fb4f03ba978564574ffc68b0d61fd3989e", size = 10643450, upload-time = "2026-04-09T14:05:42.137Z" }, + { url = "https://files.pythonhosted.org/packages/ec/76/0deec355d8ec10709653635b1f90856735302cb8e149acfdf6f82a5feb70/ruff-0.15.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8154d43684e4333360fedd11aaa40b1b08a4e37d8ffa9d95fee6fa5b37b6fab1", size = 10379597, upload-time = "2026-04-09T14:05:49.984Z" }, + { url = "https://files.pythonhosted.org/packages/dc/be/86bba8fc8798c081e28a4b3bb6d143ccad3fd5f6f024f02002b8f08a9fa3/ruff-0.15.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ab88715f3a6deb6bde6c227f3a123410bec7b855c3ae331b4c006189e895cef", size = 11146645, upload-time = "2026-04-09T14:06:12.246Z" }, + { url = "https://files.pythonhosted.org/packages/a8/89/140025e65911b281c57be1d385ba1d932c2366ca88ae6663685aed8d4881/ruff-0.15.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a768ff5969b4f44c349d48edf4ab4f91eddb27fd9d77799598e130fb628aa158", size = 12030289, upload-time = "2026-04-09T14:06:04.776Z" }, + { url = "https://files.pythonhosted.org/packages/88/de/ddacca9545a5e01332567db01d44bd8cf725f2db3b3d61a80550b48308ea/ruff-0.15.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ee3ef42dab7078bda5ff6a1bcba8539e9857deb447132ad5566a038674540d0", size = 11496266, upload-time = "2026-04-09T14:05:55.485Z" }, + { url = "https://files.pythonhosted.org/packages/bc/bb/7ddb00a83760ff4a83c4e2fc231fd63937cc7317c10c82f583302e0f6586/ruff-0.15.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51cb8cc943e891ba99989dd92d61e29b1d231e14811db9be6440ecf25d5c1609", size = 11256418, upload-time = "2026-04-09T14:05:57.69Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8d/55de0d35aacf6cd50b6ee91ee0f291672080021896543776f4170fc5c454/ruff-0.15.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:e59c9bdc056a320fb9ea1700a8d591718b8faf78af065484e801258d3a76bc3f", size = 11288416, upload-time = "2026-04-09T14:05:44.695Z" }, + { url = "https://files.pythonhosted.org/packages/68/cf/9438b1a27426ec46a80e0a718093c7f958ef72f43eb3111862949ead3cc1/ruff-0.15.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:136c00ca2f47b0018b073f28cb5c1506642a830ea941a60354b0e8bc8076b151", size = 10621053, upload-time = "2026-04-09T14:05:52.782Z" }, + { url = "https://files.pythonhosted.org/packages/4c/50/e29be6e2c135e9cd4cb15fbade49d6a2717e009dff3766dd080fcb82e251/ruff-0.15.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8b80a2f3c9c8a950d6237f2ca12b206bccff626139be9fa005f14feb881a1ae8", size = 10378302, upload-time = "2026-04-09T14:06:14.361Z" }, + { url = "https://files.pythonhosted.org/packages/18/2f/e0b36a6f99c51bb89f3a30239bc7bf97e87a37ae80aa2d6542d6e5150364/ruff-0.15.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e3e53c588164dc025b671c9df2462429d60357ea91af7e92e9d56c565a9f1b07", size = 10850074, upload-time = "2026-04-09T14:06:16.581Z" }, + { url = "https://files.pythonhosted.org/packages/11/08/874da392558ce087a0f9b709dc6ec0d60cbc694c1c772dab8d5f31efe8cb/ruff-0.15.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b0c52744cf9f143a393e284125d2576140b68264a93c6716464e129a3e9adb48", size = 11358051, upload-time = "2026-04-09T14:06:18.948Z" }, + { url = "https://files.pythonhosted.org/packages/e4/46/602938f030adfa043e67112b73821024dc79f3ab4df5474c25fa4c1d2d14/ruff-0.15.10-py3-none-win32.whl", hash = "sha256:d4272e87e801e9a27a2e8df7b21011c909d9ddd82f4f3281d269b6ba19789ca5", size = 10588964, upload-time = "2026-04-09T14:06:07.14Z" }, + { url = "https://files.pythonhosted.org/packages/25/b6/261225b875d7a13b33a6d02508c39c28450b2041bb01d0f7f1a83d569512/ruff-0.15.10-py3-none-win_amd64.whl", hash = "sha256:28cb32d53203242d403d819fd6983152489b12e4a3ae44993543d6fe62ab42ed", size = 11745044, upload-time = "2026-04-09T14:05:39.473Z" }, + { url = "https://files.pythonhosted.org/packages/58/ed/dea90a65b7d9e69888890fb14c90d7f51bf0c1e82ad800aeb0160e4bacfd/ruff-0.15.10-py3-none-win_arm64.whl", hash = "sha256:601d1610a9e1f1c2165a4f561eeaa2e2ea1e97f3287c5aa258d3dab8b57c6188", size = 11035607, upload-time = "2026-04-09T14:05:47.593Z" }, +] + [[package]] name = "salesforce-bulk" version = "2.2.0"