From f4d5f77a9cecbe6e1852d6d7c4930f0053b06074 Mon Sep 17 00:00:00 2001 From: mauritsvanrees Date: Thu, 14 Sep 2023 01:15:40 +0100 Subject: [PATCH] [fc] --- last_commit.txt | 247 ------------------------------------------------ 1 file changed, 247 deletions(-) diff --git a/last_commit.txt b/last_commit.txt index 6c12b374ad..e69de29bb2 100644 --- a/last_commit.txt +++ b/last_commit.txt @@ -1,247 +0,0 @@ -Repository: plone.releaser - - -Branch: refs/heads/master -Date: 2023-09-01T18:48:27+02:00 -Author: Maurits van Rees (mauritsvanrees) -Commit: https://github.com/plone/plone.releaser/commit/5d85a1fe667c69153cf12e2267d31df407f9f572 - -Test buildout CheckoutsFile. - -Files changed: -A plone/releaser/tests/input/checkouts.cfg -M plone/releaser/buildout.py -M plone/releaser/tests/test_buildout.py - -b'diff --git a/plone/releaser/buildout.py b/plone/releaser/buildout.py\nindex 0e3742a..5b63e3b 100644\n--- a/plone/releaser/buildout.py\n+++ b/plone/releaser/buildout.py\n@@ -181,12 +181,19 @@ def data(self):\n # Map from lower case to actual case, so we can find the package.\n mapping = {}\n for package in checkouts.splitlines():\n+ if not package:\n+ continue\n mapping[package.lower()] = package\n return mapping\n \n def __contains__(self, package_name):\n return package_name.lower() in self.data\n \n+ def __getitem__(self, package_name):\n+ if package_name in self:\n+ return self.data.get(package_name.lower())\n+ raise KeyError\n+\n def __setitem__(self, package_name, enabled=True):\n contents = self.path.read_text()\n if not contents.endswith("\\n"):\n@@ -210,6 +217,15 @@ def line_check(line):\n def __delitem__(self, package_name):\n return self.__setitem__(package_name, False)\n \n+ def get(self, package_name, default=None):\n+ if package_name in self:\n+ return self.__getitem__(package_name)\n+ return default\n+\n+ def set(self, package_name, new_version):\n+ # This method makes no sense for this class.\n+ raise NotImplementedError\n+\n def add(self, package_name):\n return self.__setitem__(package_name, True)\n \ndiff --git a/plone/releaser/tests/input/checkouts.cfg b/plone/releaser/tests/input/checkouts.cfg\nnew file mode 100644\nindex 0000000..317719d\n--- /dev/null\n+++ b/plone/releaser/tests/input/checkouts.cfg\n@@ -0,0 +1,7 @@\n+[buildout]\n+always-checkout = force\n+# always-checkout = false\n+auto-checkout =\n+# These packages are always checkout out:\n+ CamelCase\n+ package\ndiff --git a/plone/releaser/tests/test_buildout.py b/plone/releaser/tests/test_buildout.py\nindex 03f4f4e..45aa8ee 100644\n--- a/plone/releaser/tests/test_buildout.py\n+++ b/plone/releaser/tests/test_buildout.py\n@@ -1,3 +1,4 @@\n+from plone.releaser.buildout import CheckoutsFile\n from plone.releaser.buildout import VersionsFile\n \n import pathlib\n@@ -8,6 +9,68 @@\n TESTS_DIR = pathlib.Path(__file__).parent\n INPUT_DIR = TESTS_DIR / "input"\n VERSIONS_FILE = INPUT_DIR / "versions.cfg"\n+CHECKOUTS_FILE = INPUT_DIR / "checkouts.cfg"\n+\n+\n+def test_checkouts_file_data():\n+ cf = CheckoutsFile(CHECKOUTS_FILE)\n+ # The data maps lower case to actual case.\n+ assert cf.data == {\n+ "camelcase": "CamelCase",\n+ "package": "package",\n+ }\n+\n+\n+def test_checkouts_file_contains():\n+ cf = CheckoutsFile(CHECKOUTS_FILE)\n+ assert "package" in cf\n+ assert "nope" not in cf\n+ # We compare case insensitively.\n+ assert "camelcase" in cf\n+ assert "CamelCase" in cf\n+ assert "CAMELCASE" in cf\n+\n+\n+def test_checkouts_file_get():\n+ cf = CheckoutsFile(CHECKOUTS_FILE)\n+ # The data maps lower case to actual case.\n+ assert cf["package"] == "package"\n+ assert cf.get("package") == "package"\n+ assert cf["camelcase"] == "CamelCase"\n+ assert cf["CAMELCASE"] == "CamelCase"\n+ assert cf["CamelCase"] == "CamelCase"\n+ with pytest.raises(KeyError):\n+ cf["nope"]\n+\n+\n+def test_checkouts_file_add(tmp_path):\n+ # When we add or remove a checkout, the file changes, so we work on a copy.\n+ copy_path = tmp_path / "checkouts.cfg"\n+ shutil.copyfile(CHECKOUTS_FILE, copy_path)\n+ cf = CheckoutsFile(copy_path)\n+ assert "Extra" not in cf\n+ cf.add("Extra")\n+ # Let\'s read it fresh, for good measure.\n+ cf = CheckoutsFile(copy_path)\n+ assert "Extra" in cf\n+ assert cf.get("extra") == "Extra"\n+\n+\n+def test_checkouts_file_remove(tmp_path):\n+ copy_path = tmp_path / "checkouts.cfg"\n+ shutil.copyfile(CHECKOUTS_FILE, copy_path)\n+ cf = CheckoutsFile(copy_path)\n+ assert "package" in cf\n+ cf.remove("package")\n+ # Let\'s read it fresh, for good measure.\n+ cf = CheckoutsFile(copy_path)\n+ assert "package" not in cf\n+ assert "CAMELCASE" in cf\n+ cf.remove("CAMELCASE")\n+ cf = CheckoutsFile(copy_path)\n+ assert "CAMELCASE" not in cf\n+ assert "CamelCase" not in cf\n+ assert "camelcase" not in cf\n \n \n def test_versions_file_versions():\n' - -Repository: plone.releaser - - -Branch: refs/heads/master -Date: 2023-09-01T18:48:27+02:00 -Author: Maurits van Rees (mauritsvanrees) -Commit: https://github.com/plone/plone.releaser/commit/7b546ebd7c467d26c8365acadfa1c0812a595591 - -Test updating mxdev.ini, fixing some corner cases. - -Files changed: -A plone/releaser/tests/input/mxdev.ini -M plone/releaser/pip.py -M plone/releaser/tests/test_pip.py - -b'diff --git a/plone/releaser/pip.py b/plone/releaser/pip.py\nindex 4c290f3..e770036 100644\n--- a/plone/releaser/pip.py\n+++ b/plone/releaser/pip.py\n@@ -121,9 +121,24 @@ def data(self):\n checkouts[package.lower()] = package\n return checkouts\n \n+ @property\n+ def sections(self):\n+ # If we want to use a package, we must first know that it exists.\n+ sections = {}\n+ for package in self.config.sections():\n+ # Map from lower case to actual case, so we can find the package.\n+ sections[package.lower()] = package\n+ return sections\n+\n def __contains__(self, package_name):\n+ # Is the package defined AND is it used?\n return package_name.lower() in self.data\n \n+ def __getitem__(self, package_name):\n+ if package_name in self:\n+ return self.data.get(package_name.lower())\n+ raise KeyError\n+\n def __setitem__(self, package_name, enabled=True):\n """Enable or disable a checkout.\n \n@@ -133,10 +148,19 @@ def __setitem__(self, package_name, enabled=True):\n \n But let\'s support the other way around as well:\n when default-use is true, we set \'use = false\'.\n+\n+ Note that in our Buildout setup, we have sources.cfg separately.\n+ In mxdev.ini the source definition and \'use = false/true\' is combined.\n+ So if the package we want to enable is not defined, meaning it has no\n+ section, then we should fail loudly.\n """\n- stored_package_name = self.data.get(package_name.lower())\n- if stored_package_name:\n- package_name = stored_package_name\n+ stored_package_name = self.sections.get(package_name.lower())\n+ if not stored_package_name:\n+ raise KeyError(\n+ f"{self.file_location}: There is no definition for {package_name}"\n+ )\n+ package_name = stored_package_name\n+ if package_name in self:\n use = to_bool(self.config[package_name].get("use", self.default_use))\n else:\n use = False\n@@ -154,7 +178,9 @@ def __setitem__(self, package_name, enabled=True):\n \n lines = []\n found_package = False\n- for line in contents.splitlines():\n+ # Add extra line at the end. This eases parsing and editing the final section.\n+ orig_lines = contents.splitlines() + ["\\n"]\n+ for line in orig_lines:\n line = line.rstrip()\n if line == f"[{package_name}]":\n found_package = True\n@@ -188,7 +214,7 @@ def __setitem__(self, package_name, enabled=True):\n # Just a regular line.\n lines.append(line)\n \n- contents = "\\n".join(lines) + "\\n"\n+ contents = "\\n".join(lines)\n self.path.write_text(contents)\n \n def __delitem__(self, package_name):\ndiff --git a/plone/releaser/tests/input/mxdev.ini b/plone/releaser/tests/input/mxdev.ini\nnew file mode 100644\nindex 0000000..a0c15fd\n--- /dev/null\n+++ b/plone/releaser/tests/input/mxdev.ini\n@@ -0,0 +1,22 @@\n+[settings]\n+requirements-in = requirements.txt\n+requirements-out = requirements-mxdev.txt\n+contraints-out = constraints-mxdev.txt\n+# The packages are defined, but not used by default.\n+default-use = false\n+# custom variables\n+plone = https://github.com/plone\n+\n+[package]\n+url = ${settings:plone}/package.git\n+branch = main\n+use = true\n+\n+[unused]\n+url = ${settings:plone}/package.git\n+branch = main\n+\n+[CamelCase]\n+url = ${settings:plone}/CamelCase.git\n+branch = main\n+use = true\ndiff --git a/plone/releaser/tests/test_pip.py b/plone/releaser/tests/test_pip.py\nindex 4ef3d6d..5b72eb4 100644\n--- a/plone/releaser/tests/test_pip.py\n+++ b/plone/releaser/tests/test_pip.py\n@@ -1,4 +1,5 @@\n from plone.releaser.pip import ConstraintsFile\n+from plone.releaser.pip import IniFile\n \n import pathlib\n import pytest\n@@ -8,6 +9,88 @@\n TESTS_DIR = pathlib.Path(__file__).parent\n INPUT_DIR = TESTS_DIR / "input"\n CONSTRAINTS_FILE = INPUT_DIR / "constraints.txt"\n+MXDEV_FILE = INPUT_DIR / "mxdev.ini"\n+\n+\n+def test_mxdev_file_data():\n+ mf = IniFile(MXDEV_FILE)\n+ # The data maps lower case to actual case.\n+ assert mf.data == {\n+ "camelcase": "CamelCase",\n+ "package": "package",\n+ }\n+\n+\n+def test_mxdev_file_contains():\n+ mf = IniFile(MXDEV_FILE)\n+ assert "package" in mf\n+ assert "unused" not in mf\n+ # We compare case insensitively.\n+ assert "camelcase" in mf\n+ assert "CamelCase" in mf\n+ assert "CAMELCASE" in mf\n+\n+\n+def test_mxdev_file_get():\n+ mf = IniFile(MXDEV_FILE)\n+ # The data maps lower case to actual case.\n+ assert mf["package"] == "package"\n+ assert mf.get("package") == "package"\n+ assert mf["camelcase"] == "CamelCase"\n+ assert mf["CAMELCASE"] == "CamelCase"\n+ assert mf["CamelCase"] == "CamelCase"\n+ with pytest.raises(KeyError):\n+ mf["unused"]\n+ assert mf.get("unused") is None\n+\n+\n+def test_mxdev_file_add_known(tmp_path):\n+ # When we add or remove a checkout, the file changes, so we work on a copy.\n+ copy_path = tmp_path / "mxdev.ini"\n+ shutil.copyfile(MXDEV_FILE, copy_path)\n+ mf = IniFile(copy_path)\n+ assert "unused" not in mf\n+ mf.add("unused")\n+ # Let\'s read it fresh, for good measure.\n+ mf = IniFile(copy_path)\n+ assert "unused" in mf\n+ assert mf["unused"] == "unused"\n+\n+\n+def test_mxdev_file_add_unknown(tmp_path):\n+ # We cannot edit mxdev.ini to use a package when it is not defined.\n+ copy_path = tmp_path / "mxdev.ini"\n+ shutil.copyfile(MXDEV_FILE, copy_path)\n+ mf = IniFile(copy_path)\n+ assert "unknown" not in mf\n+ with pytest.raises(KeyError):\n+ mf.add("unknown")\n+\n+\n+def test_mxdev_file_remove(tmp_path):\n+ copy_path = tmp_path / "mxdev.ini"\n+ shutil.copyfile(MXDEV_FILE, copy_path)\n+ mf = IniFile(copy_path)\n+ assert "package" in mf\n+ mf.remove("package")\n+ # Let\'s read it fresh, for good measure.\n+ mf = IniFile(copy_path)\n+ assert "package" not in mf\n+ assert "CAMELCASE" in mf\n+ mf.remove("CAMELCASE")\n+ mf = IniFile(copy_path)\n+ assert "CAMELCASE" not in mf\n+ assert "CamelCase" not in mf\n+ assert "camelcase" not in mf\n+ # Check that we can re-enable a package:\n+ # editing should not remove the entire section.\n+ mf.add("package")\n+ mf = IniFile(copy_path)\n+ assert "package" in mf\n+ # This should work for the last section as well.\n+ mf.add("CamelCase")\n+ mf = IniFile(copy_path)\n+ assert "CamelCase" in mf\n \n \n def test_constraints_file_constraints():\n' - -Repository: plone.releaser - - -Branch: refs/heads/master -Date: 2023-09-01T22:17:45+02:00 -Author: Maurits van Rees (mauritsvanrees) -Commit: https://github.com/plone/plone.releaser/commit/75594aa5cabdee207f7af9d09a0371b2165c1a86 - -Make buildout.Source.create_from_string a classmethod. - -Test it. - -Files changed: -M plone/releaser/buildout.py -M plone/releaser/tests/test_buildout.py - -b'diff --git a/plone/releaser/buildout.py b/plone/releaser/buildout.py\nindex 5b63e3b..4f077cc 100644\n--- a/plone/releaser/buildout.py\n+++ b/plone/releaser/buildout.py\n@@ -15,29 +15,24 @@\n \n \n class Source:\n- def __init__(self, protocol=None, url=None, push_url=None, branch=None):\n+ def __init__(self, protocol=None, url=None, pushurl=None, branch=None):\n self.protocol = protocol\n self.url = url\n- self.push_url = push_url\n+ self.pushurl = pushurl\n self.branch = branch\n \n- def create_from_string(self, source_string):\n+ @classmethod\n+ def create_from_string(cls, source_string):\n protocol, url, extra_1, extra_2, extra_3 = (\n lambda a, b, c=None, d=None, e=None: (a, b, c, d, e)\n )(*source_string.split())\n+ # September 2023: mr.developer defaults to master, mxdev to main.\n+ options = {"protocol": protocol, "url": url, "branch": "master"}\n for param in [extra_1, extra_2, extra_3]:\n if param is not None:\n key, value = param.split("=")\n- setattr(self, key, value)\n- self.protocol = protocol\n- self.url = url\n- if self.push_url is not None:\n- self.push_url = self.push_url.split("=")[-1]\n- if self.branch is None:\n- self.branch = "master"\n- else:\n- self.branch = self.branch.split("=")[-1]\n- return self\n+ options[key] = value\n+ return cls(**options)\n \n @property\n def path(self):\n@@ -151,7 +146,7 @@ def data(self):\n sources_dict = OrderedDict()\n for name, value in config["sources"].items():\n try:\n- source = Source().create_from_string(value)\n+ source = Source.create_from_string(value)\n except TypeError:\n # Happens now for the documentation items in coredev 6.0.\n # We could print, but this gets printed a lot.\ndiff --git a/plone/releaser/tests/test_buildout.py b/plone/releaser/tests/test_buildout.py\nindex 45aa8ee..e34ed68 100644\n--- a/plone/releaser/tests/test_buildout.py\n+++ b/plone/releaser/tests/test_buildout.py\n@@ -1,4 +1,5 @@\n from plone.releaser.buildout import CheckoutsFile\n+from plone.releaser.buildout import Source\n from plone.releaser.buildout import VersionsFile\n \n import pathlib\n@@ -73,6 +74,32 @@ def test_checkouts_file_remove(tmp_path):\n assert "camelcase" not in cf\n \n \n+def test_source_standard():\n+ src = Source.create_from_string(\n+ "git https://github.com/plone/Plone.git pushurl=git@github.com:plone/Plone.git branch=6.0.x"\n+ )\n+ assert src.protocol == "git"\n+ assert src.url == "https://github.com/plone/Plone.git"\n+ assert src.pushurl == "git@github.com:plone/Plone.git"\n+ assert src.branch == "6.0.x"\n+\n+\n+def test_source_not_enough_parameters():\n+ with pytest.raises(TypeError):\n+ Source.create_from_string("")\n+ with pytest.raises(TypeError):\n+ Source.create_from_string("git")\n+\n+\n+def test_source_just_enough_parameters():\n+ # protocol and url are enough\n+ src = Source.create_from_string("git https://github.com/plone/Plone.git")\n+ assert src.protocol == "git"\n+ assert src.url == "https://github.com/plone/Plone.git"\n+ assert src.pushurl is None\n+ assert src.branch == "master"\n+\n+\n def test_versions_file_versions():\n vf = VersionsFile(VERSIONS_FILE)\n # All versions are reported lowercased.\n' - -Repository: plone.releaser - - -Branch: refs/heads/master -Date: 2023-09-01T22:34:54+02:00 -Author: Maurits van Rees (mauritsvanrees) -Commit: https://github.com/plone/plone.releaser/commit/b1e7e3d6fe1c32117e33b7d0c8c9da60bce59600 - -Removed no longer working pulls command that I have never used. - -Removed `PyGithub` and `keyring` dependencies that was only used for this. -Removed `Source.path` property that was only used for this. - -Traceback when you try the `pulls` command: - -``` -$ bin/manage pulls -Traceback (most recent call last): - File "/Users/maurits/community/plone-coredev/6.0/bin/manage", line 69, in <module> - sys.exit(plone.releaser.manage.manage()) - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - File "/Users/maurits/community/plone-coredev/6.0/src/plone.releaser/plone/releaser/manage.py", line 271, in __call__ - parser.dispatch() - File "/Users/maurits/shared-eggs/cp311/argh-0.28.1-py3.11.egg/argh/helpers.py", line 54, in dispatch - return dispatch(self, *args, **kwargs) - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - File "/Users/maurits/shared-eggs/cp311/argh-0.28.1-py3.11.egg/argh/dispatching.py", line 179, in dispatch - for line in lines: - File "/Users/maurits/shared-eggs/cp311/argh-0.28.1-py3.11.egg/argh/dispatching.py", line 290, in _execute_command - for line in result: - File "/Users/maurits/shared-eggs/cp311/argh-0.28.1-py3.11.egg/argh/dispatching.py", line 273, in _call - result = function(*positional, **keywords) - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - File "/Users/maurits/community/plone-coredev/6.0/src/plone.releaser/plone/releaser/manage.py", line 94, in pulls - g = Github(client_id=client_id, client_secret=client_secret) - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TypeError: Github.__init__() got an unexpected keyword argument 'client_id' -``` - -Files changed: -A news/56.bugfix -M plone/releaser/buildout.py -M plone/releaser/manage.py -M setup.py - -b'diff --git a/news/56.bugfix b/news/56.bugfix\nnew file mode 100644\nindex 0000000..b6d4e63\n--- /dev/null\n+++ b/news/56.bugfix\n@@ -0,0 +1,4 @@\n+Removed no longer working ``pulls`` command that I have never used.\n+Removed ``PyGithub`` and ``keyring`` dependencies that was only used for this.\n+Removed ``Source.path`` property that was only used for this.\n+[maurits]\ndiff --git a/plone/releaser/buildout.py b/plone/releaser/buildout.py\nindex 4f077cc..948f7a6 100644\n--- a/plone/releaser/buildout.py\n+++ b/plone/releaser/buildout.py\n@@ -9,11 +9,6 @@\n import re\n \n \n-PATH_RE = re.compile(\n- r"(\\w+://)(.+@)*([\\w\\d\\.]+)(:[\\d]+){0,1}/(?P.+(?=\\.git))(\\.git)"\n-)\n-\n-\n class Source:\n def __init__(self, protocol=None, url=None, pushurl=None, branch=None):\n self.protocol = protocol\n@@ -34,14 +29,6 @@ def create_from_string(cls, source_string):\n options[key] = value\n return cls(**options)\n \n- @property\n- def path(self):\n- if self.url:\n- match = PATH_RE.match(self.url)\n- if match:\n- return match.groupdict()["path"]\n- return None\n-\n \n class VersionsFile:\n def __init__(self, file_location):\ndiff --git a/plone/releaser/manage.py b/plone/releaser/manage.py\nindex 89e79a9..2345b53 100644\n--- a/plone/releaser/manage.py\n+++ b/plone/releaser/manage.py\n@@ -1,7 +1,6 @@\n from argh import arg\n from argh import ArghParser\n from argh.decorators import named\n-from github import Github\n from plone.releaser import ACTION_BATCH\n from plone.releaser import ACTION_INTERACTIVE\n from plone.releaser import ACTION_REPORT\n@@ -16,7 +15,6 @@\n from progress.bar import Bar\n \n import glob\n-import keyring\n import time\n \n \n@@ -87,22 +85,6 @@ def checkAllPackagesForUpdates(**kwargs):\n time.sleep(sleep)\n \n \n-def pulls():\n- client_id = "b9f6639835b8c9cf462a"\n- client_secret = keyring.get_password("plone.releaser", client_id)\n-\n- g = Github(client_id=client_id, client_secret=client_secret)\n-\n- for package_name, source in buildout.sources.items():\n- if source.path:\n- repo = g.get_repo(source.path)\n- pulls = [a for a in repo.get_pulls("open") if a.head.ref == source.branch]\n- if pulls:\n- print(package_name)\n- for pull in pulls:\n- print(f" {pull.user.login}: {pull.title} ({pull.url})")\n-\n-\n @named("changelog")\n @arg("--start")\n @arg("--end", default="here")\n@@ -257,7 +239,6 @@ def __call__(self, **kwargs):\n checkPypi,\n checkPackageForUpdates,\n checkAllPackagesForUpdates,\n- pulls,\n changelog,\n check_checkout,\n remove_checkout,\ndiff --git a/setup.py b/setup.py\nindex f5230c8..fe3c0fd 100644\n--- a/setup.py\n+++ b/setup.py\n@@ -41,8 +41,6 @@\n "gitpython>=3.0.0",\n "configparser",\n "progress",\n- "PyGithub",\n- "keyring",\n "zest.releaser[recommended]>=7.2.0",\n "zestreleaser.towncrier>=1.3.0",\n "docutils",\n' - -Repository: plone.releaser - - -Branch: refs/heads/master -Date: 2023-09-01T23:37:51+02:00 -Author: Maurits van Rees (mauritsvanrees) -Commit: https://github.com/plone/plone.releaser/commit/01adec7cf2aed968b2288f4f049b33eed7eac9a1 - -Source: support path and egg options. - -Then the docs and mockup sources can get parsed. - -Files changed: -M plone/releaser/buildout.py -M plone/releaser/tests/test_buildout.py - -b'diff --git a/plone/releaser/buildout.py b/plone/releaser/buildout.py\nindex 948f7a6..82eb946 100644\n--- a/plone/releaser/buildout.py\n+++ b/plone/releaser/buildout.py\n@@ -10,22 +10,39 @@\n \n \n class Source:\n- def __init__(self, protocol=None, url=None, pushurl=None, branch=None):\n+ """Source definition for mr.developer"""\n+\n+ def __init__(\n+ self, protocol=None, url=None, pushurl=None, branch=None, path=None, egg=True\n+ ):\n+ # I think mxdev only supports git as protocol.\n self.protocol = protocol\n self.url = url\n self.pushurl = pushurl\n self.branch = branch\n+ # mxdev has target (default: sources) instead of path (default: src).\n+ self.path = path\n+ # egg=True: mxdev install-mode="direct"\n+ # egg=False: mxdev install-mode="skip"\n+ self.egg = egg\n \n @classmethod\n def create_from_string(cls, source_string):\n- protocol, url, extra_1, extra_2, extra_3 = (\n- lambda a, b, c=None, d=None, e=None: (a, b, c, d, e)\n- )(*source_string.split())\n+ line_options = source_string.split()\n+ protocol = line_options.pop(0)\n+ url = line_options.pop(0)\n # September 2023: mr.developer defaults to master, mxdev to main.\n options = {"protocol": protocol, "url": url, "branch": "master"}\n- for param in [extra_1, extra_2, extra_3]:\n+\n+ # The rest of the line options are key/value pairs.\n+ for param in line_options:\n if param is not None:\n key, value = param.split("=")\n+ if key == "egg":\n+ if value.lower() in ("true", "yes", "on"):\n+ value = True\n+ elif value.lower() in ("false", "no", "off"):\n+ value = False\n options[key] = value\n return cls(**options)\n \n@@ -132,12 +149,7 @@ def data(self):\n config["buildout"]["docs-directory"] = os.path.join(os.getcwd(), "docs")\n sources_dict = OrderedDict()\n for name, value in config["sources"].items():\n- try:\n- source = Source.create_from_string(value)\n- except TypeError:\n- # Happens now for the documentation items in coredev 6.0.\n- # We could print, but this gets printed a lot.\n- continue\n+ source = Source.create_from_string(value)\n sources_dict[name] = source\n return sources_dict\n \ndiff --git a/plone/releaser/tests/test_buildout.py b/plone/releaser/tests/test_buildout.py\nindex e34ed68..52ce5b2 100644\n--- a/plone/releaser/tests/test_buildout.py\n+++ b/plone/releaser/tests/test_buildout.py\n@@ -82,12 +82,14 @@ def test_source_standard():\n assert src.url == "https://github.com/plone/Plone.git"\n assert src.pushurl == "git@github.com:plone/Plone.git"\n assert src.branch == "6.0.x"\n+ assert src.egg is True\n+ assert src.path is None\n \n \n def test_source_not_enough_parameters():\n- with pytest.raises(TypeError):\n+ with pytest.raises(IndexError):\n Source.create_from_string("")\n- with pytest.raises(TypeError):\n+ with pytest.raises(IndexError):\n Source.create_from_string("git")\n \n \n@@ -98,6 +100,21 @@ def test_source_just_enough_parameters():\n assert src.url == "https://github.com/plone/Plone.git"\n assert src.pushurl is None\n assert src.branch == "master"\n+ assert src.egg is True\n+ assert src.path is None\n+\n+\n+def test_source_docs():\n+ # Plone has a docs source with some extra options.\n+ src = Source.create_from_string(\n+ "git https://github.com/plone/documentation.git pushurl=git@github.com:plone/documentation.git egg=false branch=6.0 path=docs"\n+ )\n+ assert src.protocol == "git"\n+ assert src.url == "https://github.com/plone/documentation.git"\n+ assert src.pushurl == "git@github.com:plone/documentation.git"\n+ assert src.branch == "6.0"\n+ assert src.egg is False\n+ assert src.path == "docs"\n \n \n def test_versions_file_versions():\n' - -Repository: plone.releaser - - -Branch: refs/heads/master -Date: 2023-09-07T22:13:36+02:00 -Author: Maurits van Rees (mauritsvanrees) -Commit: https://github.com/plone/plone.releaser/commit/1fd616bec66ae43b4abcded02b06ec068533d9fa - -Test SourcesFile, add contains and get, and repr/eq on Source. - -Files changed: -A plone/releaser/tests/input/sources.cfg -M plone/releaser/buildout.py -M plone/releaser/tests/test_buildout.py - -b'diff --git a/plone/releaser/buildout.py b/plone/releaser/buildout.py\nindex 82eb946..04adee5 100644\n--- a/plone/releaser/buildout.py\n+++ b/plone/releaser/buildout.py\n@@ -46,6 +46,12 @@ def create_from_string(cls, source_string):\n options[key] = value\n return cls(**options)\n \n+ def __repr__(self):\n+ return f""\n+\n+ def __eq__(self, other):\n+ return repr(self) == repr(other)\n+\n \n class VersionsFile:\n def __init__(self, file_location):\n@@ -146,11 +152,10 @@ def data(self):\n # See this similar issue in mr.roboto:\n # https://github.com/plone/mr.roboto/issues/89\n config["buildout"]["directory"] = os.getcwd()\n- config["buildout"]["docs-directory"] = os.path.join(os.getcwd(), "docs")\n sources_dict = OrderedDict()\n for name, value in config["sources"].items():\n source = Source.create_from_string(value)\n- sources_dict[name] = source\n+ sources_dict[name.lower()] = source\n return sources_dict\n \n def __setitem__(self, package_name, value):\n@@ -159,6 +164,14 @@ def __setitem__(self, package_name, value):\n def __iter__(self):\n return self.data.__iter__()\n \n+ def __contains__(self, package_name):\n+ return package_name.lower() in self.data\n+\n+ def __getitem__(self, package_name):\n+ if package_name in self:\n+ return self.data.get(package_name.lower())\n+ raise KeyError\n+\n \n class CheckoutsFile(UserDict):\n def __init__(self, file_location):\ndiff --git a/plone/releaser/tests/input/sources.cfg b/plone/releaser/tests/input/sources.cfg\nnew file mode 100644\nindex 0000000..bd3e54e\n--- /dev/null\n+++ b/plone/releaser/tests/input/sources.cfg\n@@ -0,0 +1,16 @@\n+[buildout]\n+extends =\n+ https://raw.githubusercontent.com/zopefoundation/Zope/master/sources.cfg\n+# Define a docs directory.\n+# Must be defined in this file, otherwise mr.roboto fails when it parses only sources.cfg.\n+docs-directory = ${buildout:directory}/documentation\n+\n+[remotes]\n+plone = https://github.com/plone\n+plone_push = git@github.com:plone\n+\n+[sources]\n+docs = git ${remotes:plone}/documentation.git egg=false branch=6.0 path=${buildout:docs-directory}\n+Plone = git ${remotes:plone}/Plone.git pushurl=${remotes:plone_push}/Plone.git branch=6.0.x\n+plone.alterego = git ${remotes:plone}/plone.alterego.git\n+plone.base = git ${remotes:plone}/plone.base.git branch=main\ndiff --git a/plone/releaser/tests/test_buildout.py b/plone/releaser/tests/test_buildout.py\nindex 52ce5b2..2d0d4f6 100644\n--- a/plone/releaser/tests/test_buildout.py\n+++ b/plone/releaser/tests/test_buildout.py\n@@ -1,7 +1,9 @@\n from plone.releaser.buildout import CheckoutsFile\n from plone.releaser.buildout import Source\n+from plone.releaser.buildout import SourcesFile\n from plone.releaser.buildout import VersionsFile\n \n+import os\n import pathlib\n import pytest\n import shutil\n@@ -9,8 +11,9 @@\n \n TESTS_DIR = pathlib.Path(__file__).parent\n INPUT_DIR = TESTS_DIR / "input"\n-VERSIONS_FILE = INPUT_DIR / "versions.cfg"\n CHECKOUTS_FILE = INPUT_DIR / "checkouts.cfg"\n+SOURCES_FILE = INPUT_DIR / "sources.cfg"\n+VERSIONS_FILE = INPUT_DIR / "versions.cfg"\n \n \n def test_checkouts_file_data():\n@@ -117,6 +120,57 @@ def test_source_docs():\n assert src.path == "docs"\n \n \n+def test_sources_file_data():\n+ sf = SourcesFile(SOURCES_FILE)\n+ # Note that the keys are lowercase.\n+ assert sorted(sf.data.keys()) == ["docs", "plone", "plone.alterego", "plone.base"]\n+\n+\n+def test_sources_file_contains():\n+ sf = SourcesFile(SOURCES_FILE)\n+ assert "docs" in sf\n+ assert "plone.base" in sf\n+ assert "nope" not in sf\n+ # We compare case insensitively.\n+ assert "Plone" in sf\n+ assert "plone" in sf\n+ assert "PLONE" in sf\n+ assert "PLONE.BASE" in sf\n+\n+\n+def test_sources_file_get():\n+ sf = SourcesFile(SOURCES_FILE)\n+ with pytest.raises(KeyError):\n+ assert sf["nope"]\n+ assert sf["plone"] == sf["PLONE"]\n+ assert sf["plone"] == sf["Plone"]\n+ assert sf["plone"] != sf["plone.base"]\n+ plone = sf["plone"]\n+ assert plone.url == "https://github.com/plone/Plone.git"\n+ assert plone.pushurl == "git@github.com:plone/Plone.git"\n+ assert plone.branch == "6.0.x"\n+ assert plone.path is None\n+ assert plone.egg\n+ docs = sf["docs"]\n+ assert docs.url == "https://github.com/plone/documentation.git"\n+ assert docs.pushurl is None\n+ assert docs.branch == "6.0"\n+ assert docs.path == f"{os.getcwd()}/documentation"\n+ assert not docs.egg\n+ alterego = sf["plone.alterego"]\n+ assert alterego.url == "https://github.com/plone/plone.alterego.git"\n+ assert alterego.pushurl is None\n+ assert alterego.branch == "master"\n+ assert alterego.path is None\n+ assert alterego.egg\n+ base = sf["plone.base"]\n+ assert base.url == "https://github.com/plone/plone.base.git"\n+ assert base.pushurl is None\n+ assert base.branch == "main"\n+ assert base.path is None\n+ assert base.egg\n+\n+\n def test_versions_file_versions():\n vf = VersionsFile(VERSIONS_FILE)\n # All versions are reported lowercased.\n' - -Repository: plone.releaser - - -Branch: refs/heads/master -Date: 2023-09-07T22:22:06+02:00 -Author: Maurits van Rees (mauritsvanrees) -Commit: https://github.com/plone/plone.releaser/commit/fecdf8ad3cf2da80a143c8ead1506c44b9c63fd2 - -Let VersionsFile inherit from UserDict, and use data instead of versions. - -Closer to the others. - -Files changed: -M plone/releaser/buildout.py -M plone/releaser/tests/test_buildout.py - -b'diff --git a/plone/releaser/buildout.py b/plone/releaser/buildout.py\nindex 04adee5..ab3c87c 100644\n--- a/plone/releaser/buildout.py\n+++ b/plone/releaser/buildout.py\n@@ -53,13 +53,13 @@ def __eq__(self, other):\n return repr(self) == repr(other)\n \n \n-class VersionsFile:\n+class VersionsFile(UserDict):\n def __init__(self, file_location):\n self.file_location = file_location\n self.path = pathlib.Path(self.file_location).resolve()\n \n @property\n- def versions(self):\n+ def data(self):\n """Read the versions config.\n \n We use strict=False to avoid a DuplicateOptionError.\n@@ -92,11 +92,11 @@ def versions(self):\n return versions\n \n def __contains__(self, package_name):\n- return package_name.lower() in self.versions\n+ return package_name.lower() in self.data\n \n def __getitem__(self, package_name):\n if package_name in self:\n- return self.versions.get(package_name.lower())\n+ return self.data.get(package_name.lower())\n raise KeyError\n \n def __setitem__(self, package_name, new_version):\ndiff --git a/plone/releaser/tests/test_buildout.py b/plone/releaser/tests/test_buildout.py\nindex 2d0d4f6..eb479e8 100644\n--- a/plone/releaser/tests/test_buildout.py\n+++ b/plone/releaser/tests/test_buildout.py\n@@ -174,7 +174,7 @@ def test_sources_file_get():\n def test_versions_file_versions():\n vf = VersionsFile(VERSIONS_FILE)\n # All versions are reported lowercased.\n- assert vf.versions == {\n+ assert vf.data == {\n "annotated": "1.0",\n "camelcase": "1.0",\n "duplicate": "1.0",\n' - -Repository: plone.releaser - - -Branch: refs/heads/master -Date: 2023-09-07T22:24:14+02:00 -Author: Maurits van Rees (mauritsvanrees) -Commit: https://github.com/plone/plone.releaser/commit/92a57599c5d0d7f2be82caa26cd45d532a479083 - -Let ConstraintsFile inherit from UserDict, and use data instead of constraints. - -Closer to the others. - -Files changed: -M plone/releaser/pip.py -M plone/releaser/tests/test_pip.py - -b'diff --git a/plone/releaser/pip.py b/plone/releaser/pip.py\nindex e770036..9f86b2d 100644\n--- a/plone/releaser/pip.py\n+++ b/plone/releaser/pip.py\n@@ -16,13 +16,13 @@ def to_bool(value):\n return False\n \n \n-class ConstraintsFile:\n+class ConstraintsFile(UserDict):\n def __init__(self, file_location):\n self.file_location = file_location\n self.path = pathlib.Path(self.file_location).resolve()\n \n @cached_property\n- def constraints(self):\n+ def data(self):\n """Read the constraints."""\n contents = self.path.read_text()\n constraints = {}\n@@ -54,11 +54,11 @@ def constraints(self):\n return constraints\n \n def __contains__(self, package_name):\n- return package_name.lower() in self.constraints\n+ return package_name.lower() in self.data\n \n def __getitem__(self, package_name):\n if package_name in self:\n- return self.constraints.get(package_name.lower())\n+ return self.data.get(package_name.lower())\n raise KeyError\n \n def __setitem__(self, package_name, new_version):\ndiff --git a/plone/releaser/tests/test_pip.py b/plone/releaser/tests/test_pip.py\nindex 5b72eb4..5e256bf 100644\n--- a/plone/releaser/tests/test_pip.py\n+++ b/plone/releaser/tests/test_pip.py\n@@ -96,7 +96,7 @@ def test_mxdev_file_remove(tmp_path):\n def test_constraints_file_constraints():\n cf = ConstraintsFile(CONSTRAINTS_FILE)\n # All constraints are reported lowercased.\n- assert cf.constraints == {\n+ assert cf.data == {\n "annotated": "1.0",\n "camelcase": "1.0",\n "duplicate": "1.0",\n' - -Repository: plone.releaser - - -Branch: refs/heads/master -Date: 2023-09-07T22:49:08+02:00 -Author: Maurits van Rees (mauritsvanrees) -Commit: https://github.com/plone/plone.releaser/commit/6d0eda4ce593dbc2db30b3be14e8cdcac8a5e44f - -Add base.BaseFile as basis for most pip/buildout files. - -These classes were using lots of very similar methods. - -Files changed: -A plone/releaser/base.py -M plone/releaser/buildout.py -M plone/releaser/pip.py - -b'diff --git a/plone/releaser/base.py b/plone/releaser/base.py\nnew file mode 100644\nindex 0000000..536710f\n--- /dev/null\n+++ b/plone/releaser/base.py\n@@ -0,0 +1,45 @@\n+from collections import UserDict\n+\n+import pathlib\n+\n+\n+class BaseFile(UserDict):\n+ def __init__(self, file_location):\n+ self.file_location = file_location\n+ self.path = pathlib.Path(self.file_location).resolve()\n+\n+ @property\n+ def data(self):\n+ raise NotImplementedError\n+\n+ def __iter__(self):\n+ return self.data.__iter__()\n+\n+ def __contains__(self, package_name):\n+ return package_name.lower() in self.data\n+\n+ def __getitem__(self, package_name):\n+ if package_name in self:\n+ return self.data.get(package_name.lower())\n+ raise KeyError\n+\n+ def __setitem__(self, package_name, value):\n+ raise NotImplementedError\n+\n+ def __delitem__(self, package_name):\n+ return self.__setitem__(package_name, False)\n+\n+ def get(self, package_name, default=None):\n+ if package_name in self:\n+ return self.__getitem__(package_name)\n+ return default\n+\n+ def set(self, package_name, value):\n+ return self.__setitem__(package_name, value)\n+\n+ def add(self, package_name):\n+ # This only makes sense for files where package_name maps to True or False.\n+ return self.__setitem__(package_name, True)\n+\n+ def remove(self, package_name):\n+ return self.__delitem__(package_name)\ndiff --git a/plone/releaser/buildout.py b/plone/releaser/buildout.py\nindex ab3c87c..9ccf02d 100644\n--- a/plone/releaser/buildout.py\n+++ b/plone/releaser/buildout.py\n@@ -1,6 +1,6 @@\n+from .base import BaseFile\n from .utils import update_contents\n from collections import OrderedDict\n-from collections import UserDict\n from configparser import ConfigParser\n from configparser import ExtendedInterpolation\n \n@@ -53,7 +53,7 @@ def __eq__(self, other):\n return repr(self) == repr(other)\n \n \n-class VersionsFile(UserDict):\n+class VersionsFile(BaseFile):\n def __init__(self, file_location):\n self.file_location = file_location\n self.path = pathlib.Path(self.file_location).resolve()\n@@ -91,14 +91,6 @@ def data(self):\n versions[package] = version\n return versions\n \n- def __contains__(self, package_name):\n- return package_name.lower() in self.data\n-\n- def __getitem__(self, package_name):\n- if package_name in self:\n- return self.data.get(package_name.lower())\n- raise KeyError\n-\n def __setitem__(self, package_name, new_version):\n contents = self.path.read_text()\n if not contents.endswith("\\n"):\n@@ -127,20 +119,8 @@ def stop_check(line):\n if contents != new_contents:\n self.path.write_text(new_contents)\n \n- def get(self, package_name, default=None):\n- if package_name in self:\n- return self.__getitem__(package_name)\n- return default\n-\n- def set(self, package_name, new_version):\n- return self.__setitem__(package_name, new_version)\n-\n-\n-class SourcesFile(UserDict):\n- def __init__(self, file_location):\n- self.file_location = file_location\n- self.path = pathlib.Path(self.file_location).resolve()\n \n+class SourcesFile(BaseFile):\n @property\n def data(self):\n config = ConfigParser(interpolation=ExtendedInterpolation())\n@@ -161,23 +141,8 @@ def data(self):\n def __setitem__(self, package_name, value):\n raise NotImplementedError\n \n- def __iter__(self):\n- return self.data.__iter__()\n-\n- def __contains__(self, package_name):\n- return package_name.lower() in self.data\n-\n- def __getitem__(self, package_name):\n- if package_name in self:\n- return self.data.get(package_name.lower())\n- raise KeyError\n-\n-\n-class CheckoutsFile(UserDict):\n- def __init__(self, file_location):\n- self.file_location = file_location\n- self.path = pathlib.Path(self.file_location).resolve()\n \n+class CheckoutsFile(BaseFile):\n @property\n def data(self):\n config = ConfigParser(interpolation=ExtendedInterpolation())\n@@ -193,14 +158,6 @@ def data(self):\n mapping[package.lower()] = package\n return mapping\n \n- def __contains__(self, package_name):\n- return package_name.lower() in self.data\n-\n- def __getitem__(self, package_name):\n- if package_name in self:\n- return self.data.get(package_name.lower())\n- raise KeyError\n-\n def __setitem__(self, package_name, enabled=True):\n contents = self.path.read_text()\n if not contents.endswith("\\n"):\n@@ -221,25 +178,10 @@ def line_check(line):\n if contents != new_contents:\n self.path.write_text(new_contents)\n \n- def __delitem__(self, package_name):\n- return self.__setitem__(package_name, False)\n-\n- def get(self, package_name, default=None):\n- if package_name in self:\n- return self.__getitem__(package_name)\n- return default\n-\n def set(self, package_name, new_version):\n # This method makes no sense for this class.\n raise NotImplementedError\n \n- def add(self, package_name):\n- return self.__setitem__(package_name, True)\n-\n- def remove(self, package_name):\n- # Remove from checkouts.cfg\n- return self.__delitem__(package_name)\n-\n \n class Buildout:\n def __init__(\ndiff --git a/plone/releaser/pip.py b/plone/releaser/pip.py\nindex 9f86b2d..4024d48 100644\n--- a/plone/releaser/pip.py\n+++ b/plone/releaser/pip.py\n@@ -1,5 +1,5 @@\n+from .base import BaseFile\n from .utils import update_contents\n-from collections import UserDict\n from configparser import ConfigParser\n from configparser import ExtendedInterpolation\n from functools import cached_property\n@@ -16,11 +16,7 @@ def to_bool(value):\n return False\n \n \n-class ConstraintsFile(UserDict):\n- def __init__(self, file_location):\n- self.file_location = file_location\n- self.path = pathlib.Path(self.file_location).resolve()\n-\n+class ConstraintsFile(BaseFile):\n @cached_property\n def data(self):\n """Read the constraints."""\n@@ -53,14 +49,6 @@ def data(self):\n constraints[package] = version\n return constraints\n \n- def __contains__(self, package_name):\n- return package_name.lower() in self.data\n-\n- def __getitem__(self, package_name):\n- if package_name in self:\n- return self.data.get(package_name.lower())\n- raise KeyError\n-\n def __setitem__(self, package_name, new_version):\n contents = self.path.read_text()\n if not contents.endswith("\\n"):\n@@ -82,16 +70,8 @@ def line_check(line):\n if contents != new_contents:\n self.path.write_text(new_contents)\n \n- def get(self, package_name, default=None):\n- if package_name in self:\n- return self.__getitem__(package_name)\n- return default\n-\n- def set(self, package_name, new_version):\n- return self.__setitem__(package_name, new_version)\n-\n \n-class IniFile(UserDict):\n+class IniFile(BaseFile):\n """Ini file for mxdev.\n \n What we want to do here is similar to what we have in buildout.py\n@@ -130,15 +110,6 @@ def sections(self):\n sections[package.lower()] = package\n return sections\n \n- def __contains__(self, package_name):\n- # Is the package defined AND is it used?\n- return package_name.lower() in self.data\n-\n- def __getitem__(self, package_name):\n- if package_name in self:\n- return self.data.get(package_name.lower())\n- raise KeyError\n-\n def __setitem__(self, package_name, enabled=True):\n """Enable or disable a checkout.\n \n@@ -216,13 +187,3 @@ def __setitem__(self, package_name, enabled=True):\n \n contents = "\\n".join(lines)\n self.path.write_text(contents)\n-\n- def __delitem__(self, package_name):\n- return self.__setitem__(package_name, False)\n-\n- def add(self, package_name):\n- return self.__setitem__(package_name, True)\n-\n- def remove(self, package_name):\n- # Remove from checkouts.\n- return self.__delitem__(package_name)\n' - -Repository: plone.releaser - - -Branch: refs/heads/master -Date: 2023-09-07T22:51:27+02:00 -Author: Maurits van Rees (mauritsvanrees) -Commit: https://github.com/plone/plone.releaser/commit/7101bc9321e5073d8921e89b6ab0e98e14a40e99 - -news snippet - -Files changed: -M news/56.bugfix - -b'diff --git a/news/56.bugfix b/news/56.bugfix\nindex b6d4e63..a362c51 100644\n--- a/news/56.bugfix\n+++ b/news/56.bugfix\n@@ -1,4 +1,4 @@\n Removed no longer working ``pulls`` command that I have never used.\n-Removed ``PyGithub`` and ``keyring`` dependencies that was only used for this.\n+Removed ``PyGithub`` and ``keyring`` dependencies that were only used for this.\n Removed ``Source.path`` property that was only used for this.\n [maurits]\n' - -Repository: plone.releaser - - -Branch: refs/heads/master -Date: 2023-09-07T22:58:10+02:00 -Author: Maurits van Rees (mauritsvanrees) -Commit: https://github.com/plone/plone.releaser/commit/42a5b36f3ebe44974b922c259076e1751e2190e8 - -Use self.path more, instead of self.file_location. - -Files changed: -M plone/releaser/buildout.py -M plone/releaser/pip.py - -b'diff --git a/plone/releaser/buildout.py b/plone/releaser/buildout.py\nindex 9ccf02d..b1fca5c 100644\n--- a/plone/releaser/buildout.py\n+++ b/plone/releaser/buildout.py\n@@ -78,7 +78,7 @@ def data(self):\n So we do not want to report or edit anything except the versions section.\n """\n config = ConfigParser(interpolation=ExtendedInterpolation(), strict=False)\n- with open(self.file_location) as f:\n+ with self.path.open() as f:\n config.read_file(f)\n # https://github.com/plone/plone.releaser/issues/42\n if config.has_section("buildout"):\n@@ -125,7 +125,7 @@ class SourcesFile(BaseFile):\n def data(self):\n config = ConfigParser(interpolation=ExtendedInterpolation())\n config.optionxform = str\n- with open(self.file_location) as f:\n+ with self.path.open() as f:\n config.read_file(f)\n # We need to define a few extra variables that are in a different\n # buildout file that we do not parse here.\n@@ -146,7 +146,7 @@ class CheckoutsFile(BaseFile):\n @property\n def data(self):\n config = ConfigParser(interpolation=ExtendedInterpolation())\n- with open(self.file_location) as f:\n+ with self.path.open() as f:\n config.read_file(f)\n config["buildout"]["directory"] = os.getcwd()\n checkouts = config.get("buildout", "auto-checkout")\ndiff --git a/plone/releaser/pip.py b/plone/releaser/pip.py\nindex 4024d48..9e59b43 100644\n--- a/plone/releaser/pip.py\n+++ b/plone/releaser/pip.py\n@@ -4,7 +4,6 @@\n from configparser import ExtendedInterpolation\n from functools import cached_property\n \n-import pathlib\n import re\n \n \n@@ -81,13 +80,12 @@ class IniFile(BaseFile):\n """\n \n def __init__(self, file_location):\n- self.file_location = file_location\n- self.path = pathlib.Path(self.file_location).resolve()\n+ super().__init__(file_location)\n self.config = ConfigParser(\n default_section="settings",\n interpolation=ExtendedInterpolation(),\n )\n- with open(self.file_location) as f:\n+ with self.path.open() as f:\n self.config.read_file(f)\n self.default_use = to_bool(self.config["settings"].get("default-use", True))\n \n' - -Repository: plone.releaser - - -Branch: refs/heads/master -Date: 2023-09-14T00:48:41+02:00 -Author: Maurits van Rees (mauritsvanrees) -Commit: https://github.com/plone/plone.releaser/commit/58d0a22181b598bcee8e7c509bcebea0804b81be - -Merge pull request #56 from plone/maurits-more-tests - -Improve and test sources. Share code. - -Files changed: -A news/56.bugfix -A plone/releaser/base.py -A plone/releaser/tests/input/checkouts.cfg -A plone/releaser/tests/input/mxdev.ini -A plone/releaser/tests/input/sources.cfg -M plone/releaser/buildout.py -M plone/releaser/manage.py -M plone/releaser/pip.py -M plone/releaser/tests/test_buildout.py -M plone/releaser/tests/test_pip.py -M setup.py - -b'diff --git a/news/56.bugfix b/news/56.bugfix\nnew file mode 100644\nindex 0000000..a362c51\n--- /dev/null\n+++ b/news/56.bugfix\n@@ -0,0 +1,4 @@\n+Removed no longer working ``pulls`` command that I have never used.\n+Removed ``PyGithub`` and ``keyring`` dependencies that were only used for this.\n+Removed ``Source.path`` property that was only used for this.\n+[maurits]\ndiff --git a/plone/releaser/base.py b/plone/releaser/base.py\nnew file mode 100644\nindex 0000000..536710f\n--- /dev/null\n+++ b/plone/releaser/base.py\n@@ -0,0 +1,45 @@\n+from collections import UserDict\n+\n+import pathlib\n+\n+\n+class BaseFile(UserDict):\n+ def __init__(self, file_location):\n+ self.file_location = file_location\n+ self.path = pathlib.Path(self.file_location).resolve()\n+\n+ @property\n+ def data(self):\n+ raise NotImplementedError\n+\n+ def __iter__(self):\n+ return self.data.__iter__()\n+\n+ def __contains__(self, package_name):\n+ return package_name.lower() in self.data\n+\n+ def __getitem__(self, package_name):\n+ if package_name in self:\n+ return self.data.get(package_name.lower())\n+ raise KeyError\n+\n+ def __setitem__(self, package_name, value):\n+ raise NotImplementedError\n+\n+ def __delitem__(self, package_name):\n+ return self.__setitem__(package_name, False)\n+\n+ def get(self, package_name, default=None):\n+ if package_name in self:\n+ return self.__getitem__(package_name)\n+ return default\n+\n+ def set(self, package_name, value):\n+ return self.__setitem__(package_name, value)\n+\n+ def add(self, package_name):\n+ # This only makes sense for files where package_name maps to True or False.\n+ return self.__setitem__(package_name, True)\n+\n+ def remove(self, package_name):\n+ return self.__delitem__(package_name)\ndiff --git a/plone/releaser/buildout.py b/plone/releaser/buildout.py\nindex 0e3742a..b1fca5c 100644\n--- a/plone/releaser/buildout.py\n+++ b/plone/releaser/buildout.py\n@@ -1,6 +1,6 @@\n+from .base import BaseFile\n from .utils import update_contents\n from collections import OrderedDict\n-from collections import UserDict\n from configparser import ConfigParser\n from configparser import ExtendedInterpolation\n \n@@ -9,52 +9,57 @@\n import re\n \n \n-PATH_RE = re.compile(\n- r"(\\w+://)(.+@)*([\\w\\d\\.]+)(:[\\d]+){0,1}/(?P.+(?=\\.git))(\\.git)"\n-)\n-\n-\n class Source:\n- def __init__(self, protocol=None, url=None, push_url=None, branch=None):\n+ """Source definition for mr.developer"""\n+\n+ def __init__(\n+ self, protocol=None, url=None, pushurl=None, branch=None, path=None, egg=True\n+ ):\n+ # I think mxdev only supports git as protocol.\n self.protocol = protocol\n self.url = url\n- self.push_url = push_url\n+ self.pushurl = pushurl\n self.branch = branch\n-\n- def create_from_string(self, source_string):\n- protocol, url, extra_1, extra_2, extra_3 = (\n- lambda a, b, c=None, d=None, e=None: (a, b, c, d, e)\n- )(*source_string.split())\n- for param in [extra_1, extra_2, extra_3]:\n+ # mxdev has target (default: sources) instead of path (default: src).\n+ self.path = path\n+ # egg=True: mxdev install-mode="direct"\n+ # egg=False: mxdev install-mode="skip"\n+ self.egg = egg\n+\n+ @classmethod\n+ def create_from_string(cls, source_string):\n+ line_options = source_string.split()\n+ protocol = line_options.pop(0)\n+ url = line_options.pop(0)\n+ # September 2023: mr.developer defaults to master, mxdev to main.\n+ options = {"protocol": protocol, "url": url, "branch": "master"}\n+\n+ # The rest of the line options are key/value pairs.\n+ for param in line_options:\n if param is not None:\n key, value = param.split("=")\n- setattr(self, key, value)\n- self.protocol = protocol\n- self.url = url\n- if self.push_url is not None:\n- self.push_url = self.push_url.split("=")[-1]\n- if self.branch is None:\n- self.branch = "master"\n- else:\n- self.branch = self.branch.split("=")[-1]\n- return self\n+ if key == "egg":\n+ if value.lower() in ("true", "yes", "on"):\n+ value = True\n+ elif value.lower() in ("false", "no", "off"):\n+ value = False\n+ options[key] = value\n+ return cls(**options)\n \n- @property\n- def path(self):\n- if self.url:\n- match = PATH_RE.match(self.url)\n- if match:\n- return match.groupdict()["path"]\n- return None\n+ def __repr__(self):\n+ return f""\n+\n+ def __eq__(self, other):\n+ return repr(self) == repr(other)\n \n \n-class VersionsFile:\n+class VersionsFile(BaseFile):\n def __init__(self, file_location):\n self.file_location = file_location\n self.path = pathlib.Path(self.file_location).resolve()\n \n @property\n- def versions(self):\n+ def data(self):\n """Read the versions config.\n \n We use strict=False to avoid a DuplicateOptionError.\n@@ -73,7 +78,7 @@ def versions(self):\n So we do not want to report or edit anything except the versions section.\n """\n config = ConfigParser(interpolation=ExtendedInterpolation(), strict=False)\n- with open(self.file_location) as f:\n+ with self.path.open() as f:\n config.read_file(f)\n # https://github.com/plone/plone.releaser/issues/42\n if config.has_section("buildout"):\n@@ -86,14 +91,6 @@ def versions(self):\n versions[package] = version\n return versions\n \n- def __contains__(self, package_name):\n- return package_name.lower() in self.versions\n-\n- def __getitem__(self, package_name):\n- if package_name in self:\n- return self.versions.get(package_name.lower())\n- raise KeyError\n-\n def __setitem__(self, package_name, new_version):\n contents = self.path.read_text()\n if not contents.endswith("\\n"):\n@@ -122,71 +119,45 @@ def stop_check(line):\n if contents != new_contents:\n self.path.write_text(new_contents)\n \n- def get(self, package_name, default=None):\n- if package_name in self:\n- return self.__getitem__(package_name)\n- return default\n-\n- def set(self, package_name, new_version):\n- return self.__setitem__(package_name, new_version)\n-\n-\n-class SourcesFile(UserDict):\n- def __init__(self, file_location):\n- self.file_location = file_location\n- self.path = pathlib.Path(self.file_location).resolve()\n \n+class SourcesFile(BaseFile):\n @property\n def data(self):\n config = ConfigParser(interpolation=ExtendedInterpolation())\n config.optionxform = str\n- with open(self.file_location) as f:\n+ with self.path.open() as f:\n config.read_file(f)\n # We need to define a few extra variables that are in a different\n # buildout file that we do not parse here.\n # See this similar issue in mr.roboto:\n # https://github.com/plone/mr.roboto/issues/89\n config["buildout"]["directory"] = os.getcwd()\n- config["buildout"]["docs-directory"] = os.path.join(os.getcwd(), "docs")\n sources_dict = OrderedDict()\n for name, value in config["sources"].items():\n- try:\n- source = Source().create_from_string(value)\n- except TypeError:\n- # Happens now for the documentation items in coredev 6.0.\n- # We could print, but this gets printed a lot.\n- continue\n- sources_dict[name] = source\n+ source = Source.create_from_string(value)\n+ sources_dict[name.lower()] = source\n return sources_dict\n \n def __setitem__(self, package_name, value):\n raise NotImplementedError\n \n- def __iter__(self):\n- return self.data.__iter__()\n-\n-\n-class CheckoutsFile(UserDict):\n- def __init__(self, file_location):\n- self.file_location = file_location\n- self.path = pathlib.Path(self.file_location).resolve()\n \n+class CheckoutsFile(BaseFile):\n @property\n def data(self):\n config = ConfigParser(interpolation=ExtendedInterpolation())\n- with open(self.file_location) as f:\n+ with self.path.open() as f:\n config.read_file(f)\n config["buildout"]["directory"] = os.getcwd()\n checkouts = config.get("buildout", "auto-checkout")\n # Map from lower case to actual case, so we can find the package.\n mapping = {}\n for package in checkouts.splitlines():\n+ if not package:\n+ continue\n mapping[package.lower()] = package\n return mapping\n \n- def __contains__(self, package_name):\n- return package_name.lower() in self.data\n-\n def __setitem__(self, package_name, enabled=True):\n contents = self.path.read_text()\n if not contents.endswith("\\n"):\n@@ -207,15 +178,9 @@ def line_check(line):\n if contents != new_contents:\n self.path.write_text(new_contents)\n \n- def __delitem__(self, package_name):\n- return self.__setitem__(package_name, False)\n-\n- def add(self, package_name):\n- return self.__setitem__(package_name, True)\n-\n- def remove(self, package_name):\n- # Remove from checkouts.cfg\n- return self.__delitem__(package_name)\n+ def set(self, package_name, new_version):\n+ # This method makes no sense for this class.\n+ raise NotImplementedError\n \n \n class Buildout:\ndiff --git a/plone/releaser/manage.py b/plone/releaser/manage.py\nindex 89e79a9..2345b53 100644\n--- a/plone/releaser/manage.py\n+++ b/plone/releaser/manage.py\n@@ -1,7 +1,6 @@\n from argh import arg\n from argh import ArghParser\n from argh.decorators import named\n-from github import Github\n from plone.releaser import ACTION_BATCH\n from plone.releaser import ACTION_INTERACTIVE\n from plone.releaser import ACTION_REPORT\n@@ -16,7 +15,6 @@\n from progress.bar import Bar\n \n import glob\n-import keyring\n import time\n \n \n@@ -87,22 +85,6 @@ def checkAllPackagesForUpdates(**kwargs):\n time.sleep(sleep)\n \n \n-def pulls():\n- client_id = "b9f6639835b8c9cf462a"\n- client_secret = keyring.get_password("plone.releaser", client_id)\n-\n- g = Github(client_id=client_id, client_secret=client_secret)\n-\n- for package_name, source in buildout.sources.items():\n- if source.path:\n- repo = g.get_repo(source.path)\n- pulls = [a for a in repo.get_pulls("open") if a.head.ref == source.branch]\n- if pulls:\n- print(package_name)\n- for pull in pulls:\n- print(f" {pull.user.login}: {pull.title} ({pull.url})")\n-\n-\n @named("changelog")\n @arg("--start")\n @arg("--end", default="here")\n@@ -257,7 +239,6 @@ def __call__(self, **kwargs):\n checkPypi,\n checkPackageForUpdates,\n checkAllPackagesForUpdates,\n- pulls,\n changelog,\n check_checkout,\n remove_checkout,\ndiff --git a/plone/releaser/pip.py b/plone/releaser/pip.py\nindex 4c290f3..9e59b43 100644\n--- a/plone/releaser/pip.py\n+++ b/plone/releaser/pip.py\n@@ -1,10 +1,9 @@\n+from .base import BaseFile\n from .utils import update_contents\n-from collections import UserDict\n from configparser import ConfigParser\n from configparser import ExtendedInterpolation\n from functools import cached_property\n \n-import pathlib\n import re\n \n \n@@ -16,13 +15,9 @@ def to_bool(value):\n return False\n \n \n-class ConstraintsFile:\n- def __init__(self, file_location):\n- self.file_location = file_location\n- self.path = pathlib.Path(self.file_location).resolve()\n-\n+class ConstraintsFile(BaseFile):\n @cached_property\n- def constraints(self):\n+ def data(self):\n """Read the constraints."""\n contents = self.path.read_text()\n constraints = {}\n@@ -53,14 +48,6 @@ def constraints(self):\n constraints[package] = version\n return constraints\n \n- def __contains__(self, package_name):\n- return package_name.lower() in self.constraints\n-\n- def __getitem__(self, package_name):\n- if package_name in self:\n- return self.constraints.get(package_name.lower())\n- raise KeyError\n-\n def __setitem__(self, package_name, new_version):\n contents = self.path.read_text()\n if not contents.endswith("\\n"):\n@@ -82,16 +69,8 @@ def line_check(line):\n if contents != new_contents:\n self.path.write_text(new_contents)\n \n- def get(self, package_name, default=None):\n- if package_name in self:\n- return self.__getitem__(package_name)\n- return default\n-\n- def set(self, package_name, new_version):\n- return self.__setitem__(package_name, new_version)\n-\n \n-class IniFile(UserDict):\n+class IniFile(BaseFile):\n """Ini file for mxdev.\n \n What we want to do here is similar to what we have in buildout.py\n@@ -101,13 +80,12 @@ class IniFile(UserDict):\n """\n \n def __init__(self, file_location):\n- self.file_location = file_location\n- self.path = pathlib.Path(self.file_location).resolve()\n+ super().__init__(file_location)\n self.config = ConfigParser(\n default_section="settings",\n interpolation=ExtendedInterpolation(),\n )\n- with open(self.file_location) as f:\n+ with self.path.open() as f:\n self.config.read_file(f)\n self.default_use = to_bool(self.config["settings"].get("default-use", True))\n \n@@ -121,8 +99,14 @@ def data(self):\n checkouts[package.lower()] = package\n return checkouts\n \n- def __contains__(self, package_name):\n- return package_name.lower() in self.data\n+ @property\n+ def sections(self):\n+ # If we want to use a package, we must first know that it exists.\n+ sections = {}\n+ for package in self.config.sections():\n+ # Map from lower case to actual case, so we can find the package.\n+ sections[package.lower()] = package\n+ return sections\n \n def __setitem__(self, package_name, enabled=True):\n """Enable or disable a checkout.\n@@ -133,10 +117,19 @@ def __setitem__(self, package_name, enabled=True):\n \n But let\'s support the other way around as well:\n when default-use is true, we set \'use = false\'.\n+\n+ Note that in our Buildout setup, we have sources.cfg separately.\n+ In mxdev.ini the source definition and \'use = false/true\' is combined.\n+ So if the package we want to enable is not defined, meaning it has no\n+ section, then we should fail loudly.\n """\n- stored_package_name = self.data.get(package_name.lower())\n- if stored_package_name:\n- package_name = stored_package_name\n+ stored_package_name = self.sections.get(package_name.lower())\n+ if not stored_package_name:\n+ raise KeyError(\n+ f"{self.file_location}: There is no definition for {package_name}"\n+ )\n+ package_name = stored_package_name\n+ if package_name in self:\n use = to_bool(self.config[package_name].get("use", self.default_use))\n else:\n use = False\n@@ -154,7 +147,9 @@ def __setitem__(self, package_name, enabled=True):\n \n lines = []\n found_package = False\n- for line in contents.splitlines():\n+ # Add extra line at the end. This eases parsing and editing the final section.\n+ orig_lines = contents.splitlines() + ["\\n"]\n+ for line in orig_lines:\n line = line.rstrip()\n if line == f"[{package_name}]":\n found_package = True\n@@ -188,15 +183,5 @@ def __setitem__(self, package_name, enabled=True):\n # Just a regular line.\n lines.append(line)\n \n- contents = "\\n".join(lines) + "\\n"\n+ contents = "\\n".join(lines)\n self.path.write_text(contents)\n-\n- def __delitem__(self, package_name):\n- return self.__setitem__(package_name, False)\n-\n- def add(self, package_name):\n- return self.__setitem__(package_name, True)\n-\n- def remove(self, package_name):\n- # Remove from checkouts.\n- return self.__delitem__(package_name)\ndiff --git a/plone/releaser/tests/input/checkouts.cfg b/plone/releaser/tests/input/checkouts.cfg\nnew file mode 100644\nindex 0000000..317719d\n--- /dev/null\n+++ b/plone/releaser/tests/input/checkouts.cfg\n@@ -0,0 +1,7 @@\n+[buildout]\n+always-checkout = force\n+# always-checkout = false\n+auto-checkout =\n+# These packages are always checkout out:\n+ CamelCase\n+ package\ndiff --git a/plone/releaser/tests/input/mxdev.ini b/plone/releaser/tests/input/mxdev.ini\nnew file mode 100644\nindex 0000000..a0c15fd\n--- /dev/null\n+++ b/plone/releaser/tests/input/mxdev.ini\n@@ -0,0 +1,22 @@\n+[settings]\n+requirements-in = requirements.txt\n+requirements-out = requirements-mxdev.txt\n+contraints-out = constraints-mxdev.txt\n+# The packages are defined, but not used by default.\n+default-use = false\n+# custom variables\n+plone = https://github.com/plone\n+\n+[package]\n+url = ${settings:plone}/package.git\n+branch = main\n+use = true\n+\n+[unused]\n+url = ${settings:plone}/package.git\n+branch = main\n+\n+[CamelCase]\n+url = ${settings:plone}/CamelCase.git\n+branch = main\n+use = true\ndiff --git a/plone/releaser/tests/input/sources.cfg b/plone/releaser/tests/input/sources.cfg\nnew file mode 100644\nindex 0000000..bd3e54e\n--- /dev/null\n+++ b/plone/releaser/tests/input/sources.cfg\n@@ -0,0 +1,16 @@\n+[buildout]\n+extends =\n+ https://raw.githubusercontent.com/zopefoundation/Zope/master/sources.cfg\n+# Define a docs directory.\n+# Must be defined in this file, otherwise mr.roboto fails when it parses only sources.cfg.\n+docs-directory = ${buildout:directory}/documentation\n+\n+[remotes]\n+plone = https://github.com/plone\n+plone_push = git@github.com:plone\n+\n+[sources]\n+docs = git ${remotes:plone}/documentation.git egg=false branch=6.0 path=${buildout:docs-directory}\n+Plone = git ${remotes:plone}/Plone.git pushurl=${remotes:plone_push}/Plone.git branch=6.0.x\n+plone.alterego = git ${remotes:plone}/plone.alterego.git\n+plone.base = git ${remotes:plone}/plone.base.git branch=main\ndiff --git a/plone/releaser/tests/test_buildout.py b/plone/releaser/tests/test_buildout.py\nindex 03f4f4e..eb479e8 100644\n--- a/plone/releaser/tests/test_buildout.py\n+++ b/plone/releaser/tests/test_buildout.py\n@@ -1,5 +1,9 @@\n+from plone.releaser.buildout import CheckoutsFile\n+from plone.releaser.buildout import Source\n+from plone.releaser.buildout import SourcesFile\n from plone.releaser.buildout import VersionsFile\n \n+import os\n import pathlib\n import pytest\n import shutil\n@@ -7,13 +11,170 @@\n \n TESTS_DIR = pathlib.Path(__file__).parent\n INPUT_DIR = TESTS_DIR / "input"\n+CHECKOUTS_FILE = INPUT_DIR / "checkouts.cfg"\n+SOURCES_FILE = INPUT_DIR / "sources.cfg"\n VERSIONS_FILE = INPUT_DIR / "versions.cfg"\n \n \n+def test_checkouts_file_data():\n+ cf = CheckoutsFile(CHECKOUTS_FILE)\n+ # The data maps lower case to actual case.\n+ assert cf.data == {\n+ "camelcase": "CamelCase",\n+ "package": "package",\n+ }\n+\n+\n+def test_checkouts_file_contains():\n+ cf = CheckoutsFile(CHECKOUTS_FILE)\n+ assert "package" in cf\n+ assert "nope" not in cf\n+ # We compare case insensitively.\n+ assert "camelcase" in cf\n+ assert "CamelCase" in cf\n+ assert "CAMELCASE" in cf\n+\n+\n+def test_checkouts_file_get():\n+ cf = CheckoutsFile(CHECKOUTS_FILE)\n+ # The data maps lower case to actual case.\n+ assert cf["package"] == "package"\n+ assert cf.get("package") == "package"\n+ assert cf["camelcase"] == "CamelCase"\n+ assert cf["CAMELCASE"] == "CamelCase"\n+ assert cf["CamelCase"] == "CamelCase"\n+ with pytest.raises(KeyError):\n+ cf["nope"]\n+\n+\n+def test_checkouts_file_add(tmp_path):\n+ # When we add or remove a checkout, the file changes, so we work on a copy.\n+ copy_path = tmp_path / "checkouts.cfg"\n+ shutil.copyfile(CHECKOUTS_FILE, copy_path)\n+ cf = CheckoutsFile(copy_path)\n+ assert "Extra" not in cf\n+ cf.add("Extra")\n+ # Let\'s read it fresh, for good measure.\n+ cf = CheckoutsFile(copy_path)\n+ assert "Extra" in cf\n+ assert cf.get("extra") == "Extra"\n+\n+\n+def test_checkouts_file_remove(tmp_path):\n+ copy_path = tmp_path / "checkouts.cfg"\n+ shutil.copyfile(CHECKOUTS_FILE, copy_path)\n+ cf = CheckoutsFile(copy_path)\n+ assert "package" in cf\n+ cf.remove("package")\n+ # Let\'s read it fresh, for good measure.\n+ cf = CheckoutsFile(copy_path)\n+ assert "package" not in cf\n+ assert "CAMELCASE" in cf\n+ cf.remove("CAMELCASE")\n+ cf = CheckoutsFile(copy_path)\n+ assert "CAMELCASE" not in cf\n+ assert "CamelCase" not in cf\n+ assert "camelcase" not in cf\n+\n+\n+def test_source_standard():\n+ src = Source.create_from_string(\n+ "git https://github.com/plone/Plone.git pushurl=git@github.com:plone/Plone.git branch=6.0.x"\n+ )\n+ assert src.protocol == "git"\n+ assert src.url == "https://github.com/plone/Plone.git"\n+ assert src.pushurl == "git@github.com:plone/Plone.git"\n+ assert src.branch == "6.0.x"\n+ assert src.egg is True\n+ assert src.path is None\n+\n+\n+def test_source_not_enough_parameters():\n+ with pytest.raises(IndexError):\n+ Source.create_from_string("")\n+ with pytest.raises(IndexError):\n+ Source.create_from_string("git")\n+\n+\n+def test_source_just_enough_parameters():\n+ # protocol and url are enough\n+ src = Source.create_from_string("git https://github.com/plone/Plone.git")\n+ assert src.protocol == "git"\n+ assert src.url == "https://github.com/plone/Plone.git"\n+ assert src.pushurl is None\n+ assert src.branch == "master"\n+ assert src.egg is True\n+ assert src.path is None\n+\n+\n+def test_source_docs():\n+ # Plone has a docs source with some extra options.\n+ src = Source.create_from_string(\n+ "git https://github.com/plone/documentation.git pushurl=git@github.com:plone/documentation.git egg=false branch=6.0 path=docs"\n+ )\n+ assert src.protocol == "git"\n+ assert src.url == "https://github.com/plone/documentation.git"\n+ assert src.pushurl == "git@github.com:plone/documentation.git"\n+ assert src.branch == "6.0"\n+ assert src.egg is False\n+ assert src.path == "docs"\n+\n+\n+def test_sources_file_data():\n+ sf = SourcesFile(SOURCES_FILE)\n+ # Note that the keys are lowercase.\n+ assert sorted(sf.data.keys()) == ["docs", "plone", "plone.alterego", "plone.base"]\n+\n+\n+def test_sources_file_contains():\n+ sf = SourcesFile(SOURCES_FILE)\n+ assert "docs" in sf\n+ assert "plone.base" in sf\n+ assert "nope" not in sf\n+ # We compare case insensitively.\n+ assert "Plone" in sf\n+ assert "plone" in sf\n+ assert "PLONE" in sf\n+ assert "PLONE.BASE" in sf\n+\n+\n+def test_sources_file_get():\n+ sf = SourcesFile(SOURCES_FILE)\n+ with pytest.raises(KeyError):\n+ assert sf["nope"]\n+ assert sf["plone"] == sf["PLONE"]\n+ assert sf["plone"] == sf["Plone"]\n+ assert sf["plone"] != sf["plone.base"]\n+ plone = sf["plone"]\n+ assert plone.url == "https://github.com/plone/Plone.git"\n+ assert plone.pushurl == "git@github.com:plone/Plone.git"\n+ assert plone.branch == "6.0.x"\n+ assert plone.path is None\n+ assert plone.egg\n+ docs = sf["docs"]\n+ assert docs.url == "https://github.com/plone/documentation.git"\n+ assert docs.pushurl is None\n+ assert docs.branch == "6.0"\n+ assert docs.path == f"{os.getcwd()}/documentation"\n+ assert not docs.egg\n+ alterego = sf["plone.alterego"]\n+ assert alterego.url == "https://github.com/plone/plone.alterego.git"\n+ assert alterego.pushurl is None\n+ assert alterego.branch == "master"\n+ assert alterego.path is None\n+ assert alterego.egg\n+ base = sf["plone.base"]\n+ assert base.url == "https://github.com/plone/plone.base.git"\n+ assert base.pushurl is None\n+ assert base.branch == "main"\n+ assert base.path is None\n+ assert base.egg\n+\n+\n def test_versions_file_versions():\n vf = VersionsFile(VERSIONS_FILE)\n # All versions are reported lowercased.\n- assert vf.versions == {\n+ assert vf.data == {\n "annotated": "1.0",\n "camelcase": "1.0",\n "duplicate": "1.0",\ndiff --git a/plone/releaser/tests/test_pip.py b/plone/releaser/tests/test_pip.py\nindex 4ef3d6d..5e256bf 100644\n--- a/plone/releaser/tests/test_pip.py\n+++ b/plone/releaser/tests/test_pip.py\n@@ -1,4 +1,5 @@\n from plone.releaser.pip import ConstraintsFile\n+from plone.releaser.pip import IniFile\n \n import pathlib\n import pytest\n@@ -8,12 +9,94 @@\n TESTS_DIR = pathlib.Path(__file__).parent\n INPUT_DIR = TESTS_DIR / "input"\n CONSTRAINTS_FILE = INPUT_DIR / "constraints.txt"\n+MXDEV_FILE = INPUT_DIR / "mxdev.ini"\n+\n+\n+def test_mxdev_file_data():\n+ mf = IniFile(MXDEV_FILE)\n+ # The data maps lower case to actual case.\n+ assert mf.data == {\n+ "camelcase": "CamelCase",\n+ "package": "package",\n+ }\n+\n+\n+def test_mxdev_file_contains():\n+ mf = IniFile(MXDEV_FILE)\n+ assert "package" in mf\n+ assert "unused" not in mf\n+ # We compare case insensitively.\n+ assert "camelcase" in mf\n+ assert "CamelCase" in mf\n+ assert "CAMELCASE" in mf\n+\n+\n+def test_mxdev_file_get():\n+ mf = IniFile(MXDEV_FILE)\n+ # The data maps lower case to actual case.\n+ assert mf["package"] == "package"\n+ assert mf.get("package") == "package"\n+ assert mf["camelcase"] == "CamelCase"\n+ assert mf["CAMELCASE"] == "CamelCase"\n+ assert mf["CamelCase"] == "CamelCase"\n+ with pytest.raises(KeyError):\n+ mf["unused"]\n+ assert mf.get("unused") is None\n+\n+\n+def test_mxdev_file_add_known(tmp_path):\n+ # When we add or remove a checkout, the file changes, so we work on a copy.\n+ copy_path = tmp_path / "mxdev.ini"\n+ shutil.copyfile(MXDEV_FILE, copy_path)\n+ mf = IniFile(copy_path)\n+ assert "unused" not in mf\n+ mf.add("unused")\n+ # Let\'s read it fresh, for good measure.\n+ mf = IniFile(copy_path)\n+ assert "unused" in mf\n+ assert mf["unused"] == "unused"\n+\n+\n+def test_mxdev_file_add_unknown(tmp_path):\n+ # We cannot edit mxdev.ini to use a package when it is not defined.\n+ copy_path = tmp_path / "mxdev.ini"\n+ shutil.copyfile(MXDEV_FILE, copy_path)\n+ mf = IniFile(copy_path)\n+ assert "unknown" not in mf\n+ with pytest.raises(KeyError):\n+ mf.add("unknown")\n+\n+\n+def test_mxdev_file_remove(tmp_path):\n+ copy_path = tmp_path / "mxdev.ini"\n+ shutil.copyfile(MXDEV_FILE, copy_path)\n+ mf = IniFile(copy_path)\n+ assert "package" in mf\n+ mf.remove("package")\n+ # Let\'s read it fresh, for good measure.\n+ mf = IniFile(copy_path)\n+ assert "package" not in mf\n+ assert "CAMELCASE" in mf\n+ mf.remove("CAMELCASE")\n+ mf = IniFile(copy_path)\n+ assert "CAMELCASE" not in mf\n+ assert "CamelCase" not in mf\n+ assert "camelcase" not in mf\n+ # Check that we can re-enable a package:\n+ # editing should not remove the entire section.\n+ mf.add("package")\n+ mf = IniFile(copy_path)\n+ assert "package" in mf\n+ # This should work for the last section as well.\n+ mf.add("CamelCase")\n+ mf = IniFile(copy_path)\n+ assert "CamelCase" in mf\n \n \n def test_constraints_file_constraints():\n cf = ConstraintsFile(CONSTRAINTS_FILE)\n # All constraints are reported lowercased.\n- assert cf.constraints == {\n+ assert cf.data == {\n "annotated": "1.0",\n "camelcase": "1.0",\n "duplicate": "1.0",\ndiff --git a/setup.py b/setup.py\nindex f5230c8..fe3c0fd 100644\n--- a/setup.py\n+++ b/setup.py\n@@ -41,8 +41,6 @@\n "gitpython>=3.0.0",\n "configparser",\n "progress",\n- "PyGithub",\n- "keyring",\n "zest.releaser[recommended]>=7.2.0",\n "zestreleaser.towncrier>=1.3.0",\n "docutils",\n' -