From d659ea0ed0f8518608f40065aa5e6ad6dd2a69be Mon Sep 17 00:00:00 2001 From: Tom Most Date: Sat, 27 Jul 2024 16:37:24 -0700 Subject: [PATCH] There and back again --- src/incremental/__init__.py | 59 ++++++------------ src/incremental/_hatch.py | 2 +- src/incremental/tests/test_pyproject.py | 83 ++++++------------------- 3 files changed, 39 insertions(+), 105 deletions(-) diff --git a/src/incremental/__init__.py b/src/incremental/__init__.py index d42e4d5..7da0359 100644 --- a/src/incremental/__init__.py +++ b/src/incremental/__init__.py @@ -368,7 +368,7 @@ def _findPath(path, package): # type: (str, str) -> str return current_dir else: raise ValueError( - "Can't find the directory of package {}: I looked in {} and {}".format( + "Can't find the directory of project {}: I looked in {} and {}".format( package, src_dir, current_dir ) ) @@ -404,9 +404,13 @@ def _get_setuptools_version(dist): # type: (_Distribution) -> None but this hook is always called before setuptools loads anything from ``pyproject.toml``. """ - # When operating in a packaging context (i.e. building an sdist or wheel) - # pyproject.toml will always be found in the current working directory. - config = _load_pyproject_toml("./pyproject.toml", opt_in=False) + try: + # When operating in a packaging context (i.e. building an sdist or + # wheel) pyproject.toml will always be found in the current working + # directory. + config = _load_pyproject_toml("./pyproject.toml") + except Exception: + return if not config or not config.opt_in: return @@ -466,13 +470,13 @@ class _IncrementalConfig: """ package: str - """The package name, capitalized as in the package metadata.""" + """The project name, capitalized as in the project metadata.""" path: str """Path to the package root""" -def _load_pyproject_toml(toml_path, opt_in): # type: (str, bool) -> Optional[_IncrementalConfig] +def _load_pyproject_toml(toml_path): # type: (str) -> Optional[_IncrementalConfig] """ Load Incremental configuration from a ``pyproject.toml`` @@ -483,31 +487,11 @@ def _load_pyproject_toml(toml_path, opt_in): # type: (str, bool) -> Optional[_I @param toml_path: Path to the ``pyproject.toml`` to load. - - @param opt_in: - Are we operating in a context where Incremental has been - affirmatively requested? - - Otherwise we do our best to *never* raise an exception until we - find a ``[tool.incremental]`` opt-in. This is important when - operating within a setuptools entry point because those hooks - are invoked anytime the L{Distribution} class is initialized, - which happens in non-packaging contexts that don't match the """ - try: - with open(toml_path, "rb") as f: - data = _load_toml(f) - except Exception: - if opt_in: - raise - return None - - tool_incremental = _extract_tool_incremental(data, opt_in) + with open(toml_path, "rb") as f: + data = _load_toml(f) - # Do we have an affirmative opt-in to use Incremental? - opt_in = opt_in or tool_incremental is not None - if not opt_in: - return None + tool_incremental = _extract_tool_incremental(data) # Extract the project name package = None @@ -522,7 +506,7 @@ def _load_pyproject_toml(toml_path, opt_in): # type: (str, bool) -> Optional[_I if package is None: # We can't proceed without a project name. raise ValueError("""\ -Incremental failed to extract the package name from pyproject.toml. Specify it like: +Incremental failed to extract the project name from pyproject.toml. Specify it like: [project] name = "Foo" @@ -535,33 +519,28 @@ def _load_pyproject_toml(toml_path, opt_in): # type: (str, bool) -> Optional[_I """) if not isinstance(package, str): raise TypeError( - "Package name must be a string, but found {}".format(type(package)) + "The project name must be a string, but found {}".format(type(package)) ) return _IncrementalConfig( - opt_in=opt_in, + opt_in=tool_incremental is not None, package=package, path=_findPath(os.path.dirname(toml_path), package), ) -def _extract_tool_incremental(data, opt_in): # type: (Dict[str, object], bool) -> Optional[Dict[str, object]] +def _extract_tool_incremental(data): # type: (Dict[str, object]) -> Optional[Dict[str, object]] if "tool" not in data: return None if not isinstance(data["tool"], dict): - if opt_in: - raise ValueError("[tool] must be a table") - return None + raise ValueError("[tool] must be a table") if "incremental" not in data["tool"]: return None tool_incremental = data["tool"]["incremental"] if not isinstance(tool_incremental, dict): - if opt_in: - raise ValueError("[tool.incremental] must be a table") - return None + raise ValueError("[tool.incremental] must be a table") - # At this point we've found a [tool.incremental] table, so we have opt_in if not {"name"}.issuperset(tool_incremental.keys()): raise ValueError("Unexpected key(s) in [tool.incremental]") return tool_incremental diff --git a/src/incremental/_hatch.py b/src/incremental/_hatch.py index 0305073..7777adc 100644 --- a/src/incremental/_hatch.py +++ b/src/incremental/_hatch.py @@ -21,7 +21,7 @@ class IncrementalVersionSource(VersionSourceInterface): def get_version_data(self) -> _VersionData: # type: ignore[override] path = os.path.join(self.root, "./pyproject.toml") # If the Hatch plugin is running at all we've already opted in. - config = _load_pyproject_toml(path, opt_in=True) + config = _load_pyproject_toml(path) assert config is not None, "Failed to read {}".format(path) return {"version": _existing_version(config.path).public()} diff --git a/src/incremental/tests/test_pyproject.py b/src/incremental/tests/test_pyproject.py index 939d8b8..d0f1c0b 100644 --- a/src/incremental/tests/test_pyproject.py +++ b/src/incremental/tests/test_pyproject.py @@ -15,7 +15,7 @@ class VerifyPyprojectDotTomlTests(TestCase): """Test the `_load_pyproject_toml` helper function""" def _loadToml( - self, toml: str, opt_in: bool, *, path: Union[Path, str, None] = None + self, toml: str, *, path: Union[Path, str, None] = None ) -> Optional[_IncrementalConfig]: """ Read a TOML snipped from a temporary file with `_load_pyproject_toml` @@ -34,7 +34,7 @@ def _loadToml( f.write(toml) try: - return _load_pyproject_toml(path_, opt_in) + return _load_pyproject_toml(path_) except Exception as e: if hasattr(e, "add_note"): e.add_note( # type: ignore[attr-defined] @@ -48,50 +48,27 @@ def test_fileNotFound(self): there is opt-in. """ path = os.path.join(cast(str, self.mktemp()), "pyproject.toml") - self.assertIsNone(_load_pyproject_toml(path, False)) - self.assertRaises(FileNotFoundError, _load_pyproject_toml, path, True) + self.assertRaises(FileNotFoundError, _load_pyproject_toml, path) def test_brokenToml(self): """ Syntactially invalid TOML is ignored unless there's an opt-in. """ toml = '[project]\nname = "abc' # truncated + self.assertRaises(Exception, self._loadToml, toml) - self.assertIsNone(self._loadToml(toml, False)) - self.assertRaises(Exception, self._loadToml, toml, True) - - def test_configMissing(self): + def test_nameMissing(self): """ - A ``pyproject.toml`` that exists but provides no relevant configuration - is ignored unless opted in. + `ValueError` is raised when we can't extract the project name. """ for toml in [ "\n", "[tool.notincremental]\n", "[project]\n", - ]: - self.assertIsNone(self._loadToml(toml, False)) - - def test_nameMissing(self): - """ - `ValueError` is raised when ``[tool.incremental]`` is present but - the project name isn't available. The ``[tool.incremental]`` - section counts as opt-in. - """ - for toml in [ "[tool.incremental]\n", "[project]\n[tool.incremental]\n", ]: - self.assertRaises(ValueError, self._loadToml, toml, False) - self.assertRaises(ValueError, self._loadToml, toml, True) - - def test_nameInvalidNoOptIn(self): - """ - An invalid project name is ignored when there's no opt-in. - """ - self.assertIsNone( - self._loadToml("[project]\nname = false\n", False), - ) + self.assertRaises(ValueError, self._loadToml, toml) def test_nameInvalidOptIn(self): """ @@ -103,7 +80,8 @@ def test_nameInvalidOptIn(self): "[tool.incremental]\nname = -1\n", "[tool.incremental]\n[project]\nname = 1.0\n", ]: - self.assertRaises(TypeError, self._loadToml, toml, True) + with self.assertRaisesRegex(TypeError, "The project name must be a string"): + self._loadToml(toml) def test_toolIncrementalInvalid(self): """ @@ -117,8 +95,7 @@ def test_toolIncrementalInvalid(self): "[tool]\nincremental = 123\n", "[tool]\nincremental = null\n", ]: - self.assertIsNone(self._loadToml(toml, False)) - self.assertRaises(ValueError, self._loadToml, toml, True) + self.assertRaises(ValueError, self._loadToml, toml) def test_toolIncrementalUnexpecteKeys(self): """ @@ -129,7 +106,7 @@ def test_toolIncrementalUnexpecteKeys(self): "[tool.incremental]\nfoo = false\n", '[tool.incremental]\nname = "OK"\nother = false\n', ]: - self.assertRaises(ValueError, self._loadToml, toml, False) + self.assertRaises(ValueError, self._loadToml, toml) def test_setuptoolsOptIn(self): """ @@ -145,7 +122,7 @@ def test_setuptoolsOptIn(self): '[project]\nname = "Foo"\n[tool.incremental]\n', '[tool.incremental]\nname = "Foo"\n', ]: - config = self._loadToml(toml, False, path=root / "pyproject.toml") + config = self._loadToml(toml, path=root / "pyproject.toml") self.assertEqual( config, @@ -156,44 +133,23 @@ def test_setuptoolsOptIn(self): ), ) - def test_noToolIncrementalSection(self): + def test_packagePathRequired(self): """ - We don't produce config unless we find opt-in. - - The ``[project]`` section doesn't imply opt-in, even if we can - recover the project name from it. - """ - root = Path(self.mktemp()) - pkg = root / "foo" # A valid package directory. - pkg.mkdir(parents=True) - - config = self._loadToml( - '[project]\nname = "foo"\n', - opt_in=False, - path=root / "pyproject.toml", - ) - - self.assertIsNone(config) - - def test_pathNotFoundOptIn(self): - """ - Once opted in, raise `ValueError` when the package root can't - be resolved. + Raise `ValueError` when the package root can't be resolved. """ root = Path(self.mktemp()) root.mkdir() # Contains no package directory. - with self.assertRaisesRegex(ValueError, "Can't find the directory of package"): + with self.assertRaisesRegex(ValueError, "Can't find the directory of project "): self._loadToml( '[project]\nname = "foo"\n', - opt_in=True, path=root / "pyproject.toml", ) - def test_noToolIncrementalSectionOptIn(self): + def test_noToolIncrementalSection(self): """ - If opted in (i.e. in the Hatch plugin) then the [tool.incremental] - table is completely optional. + The ``[tool.incremental]`` table is not strictly required, but its + ``opt_in=False`` indicates its absence. """ root = Path(self.mktemp()) pkg = root / "src" / "foo" @@ -201,14 +157,13 @@ def test_noToolIncrementalSectionOptIn(self): config = self._loadToml( '[project]\nname = "Foo"\n', - opt_in=True, path=root / "pyproject.toml", ) self.assertEqual( config, _IncrementalConfig( - opt_in=True, + opt_in=False, package="Foo", path=str(pkg), ),