From 17b6a07bd1be08ec6501378644c07c9281775de0 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Mon, 18 Dec 2023 11:58:11 +1100 Subject: [PATCH 1/2] Add ':parent' path modifier Add a new `:parent` modifier for path. This can be specified any numbers of times, with each addition stripping one level from the path. This is useful in the context of a monorepo to help normalise paths between sibling packages. Resolves: #825 Signed-off-by: JP-Ellis --- backend/src/hatchling/utils/context.py | 4 ++++ docs/config/context.md | 3 +++ tests/backend/utils/test_context.py | 23 ++++++++++++++++++++++- 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/backend/src/hatchling/utils/context.py b/backend/src/hatchling/utils/context.py index 26816cec1..1119d14d1 100644 --- a/backend/src/hatchling/utils/context.py +++ b/backend/src/hatchling/utils/context.py @@ -26,6 +26,10 @@ def format_path(cls, path: str, modifier: str) -> str: if not modifier: return os.path.normpath(path) + while modifier.endswith(':parent'): + path = os.path.dirname(path) + modifier = modifier[:-7] + if modifier == 'uri': return path_to_uri(path) diff --git a/docs/config/context.md b/docs/config/context.md index b8d835c57..45f3f259c 100644 --- a/docs/config/context.md +++ b/docs/config/context.md @@ -21,6 +21,9 @@ All paths support the following modifiers: | --- | --- | | `uri` | The normalized absolute URI path prefixed by `file:` | | `real` | The path with all symbolic links resolved | +| `parent` | The parent of the previous path | + +Note that the `parent` modifier can be chained to get the parent of the parent of the parent etc. It must be combined with either the `uri` or `real` modifier. ### System separators diff --git a/tests/backend/utils/test_context.py b/tests/backend/utils/test_context.py index 1eb13be0c..fffaf55a2 100644 --- a/tests/backend/utils/test_context.py +++ b/tests/backend/utils/test_context.py @@ -31,9 +31,30 @@ def test_uri(self, isolation, uri_slash_prefix): normalized_path = str(isolation).replace(os.sep, '/') assert context.format('foo {root:uri}') == f'foo file:{uri_slash_prefix}{normalized_path}' + def test_uri_parent(self, isolation, uri_slash_prefix): + context = Context(isolation) + normalized_path = os.path.dirname(str(isolation)).replace(os.sep, '/') + assert context.format('foo {root:uri:parent}') == f'foo file:{uri_slash_prefix}{normalized_path}' + + def test_uri_parent_parent(self, isolation, uri_slash_prefix): + context = Context(isolation) + normalized_path = os.path.dirname(os.path.dirname(str(isolation))).replace(os.sep, '/') + assert context.format('foo {root:uri:parent:parent}') == f'foo file:{uri_slash_prefix}{normalized_path}' + def test_real(self, isolation): context = Context(isolation) - assert context.format('foo {root:real}') == f'foo {os.path.realpath(isolation)}' + real_path = os.path.realpath(isolation) + assert context.format('foo {root:real}') == f'foo {real_path}' + + def test_real_parent(self, isolation): + context = Context(isolation) + real_path = os.path.dirname(os.path.realpath(isolation)) + assert context.format('foo {root:real:parent}') == f'foo {real_path}' + + def test_real_parent_parent(self, isolation): + context = Context(isolation) + real_path = os.path.dirname(os.path.dirname(os.path.realpath(isolation))) + assert context.format('foo {root:real:parent:parent}') == f'foo {real_path}' def test_unknown_modifier(self, isolation): context = Context(isolation) From 5490ee8f20df8a08ac17a47f724daea03130d637 Mon Sep 17 00:00:00 2001 From: Ofek Lev Date: Sun, 17 Dec 2023 22:25:50 -0500 Subject: [PATCH 2/2] update --- backend/src/hatchling/utils/context.py | 13 +++++++++++-- docs/config/context.md | 12 ++++++++++-- docs/history/hatchling.md | 1 + tests/backend/utils/test_context.py | 24 ++++++++++++++++++++---- 4 files changed, 42 insertions(+), 8 deletions(-) diff --git a/backend/src/hatchling/utils/context.py b/backend/src/hatchling/utils/context.py index 1119d14d1..3006c0676 100644 --- a/backend/src/hatchling/utils/context.py +++ b/backend/src/hatchling/utils/context.py @@ -26,10 +26,19 @@ def format_path(cls, path: str, modifier: str) -> str: if not modifier: return os.path.normpath(path) - while modifier.endswith(':parent'): + modifiers = modifier.split(':')[::-1] + while modifiers and modifiers[-1] == 'parent': path = os.path.dirname(path) - modifier = modifier[:-7] + modifiers.pop() + if not modifiers: + return path + + if len(modifiers) > 1: + message = f'Expected a single path modifier and instead got: {", ".join(reversed(modifiers))}' + raise ValueError(message) + + modifier = modifiers[0] if modifier == 'uri': return path_to_uri(path) diff --git a/docs/config/context.md b/docs/config/context.md index 45f3f259c..46b4b0414 100644 --- a/docs/config/context.md +++ b/docs/config/context.md @@ -21,9 +21,17 @@ All paths support the following modifiers: | --- | --- | | `uri` | The normalized absolute URI path prefixed by `file:` | | `real` | The path with all symbolic links resolved | -| `parent` | The parent of the previous path | +| `parent` | The parent of the preceding path | -Note that the `parent` modifier can be chained to get the parent of the parent of the parent etc. It must be combined with either the `uri` or `real` modifier. +!!! tip + The `parent` modifier can be chained and may be combined with either the `uri` or `real` modifier, with the latter placed at the end. For example: + + ```toml config-example + [tool.hatch.envs.test] + dependencies = [ + "example-project @ {root:parent:parent:uri}/example-project", + ] + ``` ### System separators diff --git a/docs/history/hatchling.md b/docs/history/hatchling.md index e2248eb40..8325acc8f 100644 --- a/docs/history/hatchling.md +++ b/docs/history/hatchling.md @@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ***Added:*** - Add `bypass-selection` option to the `wheel` build target to allow for empty (metadata-only) wheels +- Add `parent` context modifier for path fields ***Fixed:*** diff --git a/tests/backend/utils/test_context.py b/tests/backend/utils/test_context.py index fffaf55a2..65d43f864 100644 --- a/tests/backend/utils/test_context.py +++ b/tests/backend/utils/test_context.py @@ -26,6 +26,16 @@ def test_default(self, isolation): context = Context(isolation) assert context.format('foo {root}') == f'foo {isolation}' + def test_parent(self, isolation): + context = Context(isolation) + path = os.path.dirname(str(isolation)) + assert context.format('foo {root:parent}') == f'foo {path}' + + def test_parent_parent(self, isolation): + context = Context(isolation) + path = os.path.dirname(os.path.dirname(str(isolation))) + assert context.format('foo {root:parent:parent}') == f'foo {path}' + def test_uri(self, isolation, uri_slash_prefix): context = Context(isolation) normalized_path = str(isolation).replace(os.sep, '/') @@ -34,12 +44,12 @@ def test_uri(self, isolation, uri_slash_prefix): def test_uri_parent(self, isolation, uri_slash_prefix): context = Context(isolation) normalized_path = os.path.dirname(str(isolation)).replace(os.sep, '/') - assert context.format('foo {root:uri:parent}') == f'foo file:{uri_slash_prefix}{normalized_path}' + assert context.format('foo {root:parent:uri}') == f'foo file:{uri_slash_prefix}{normalized_path}' def test_uri_parent_parent(self, isolation, uri_slash_prefix): context = Context(isolation) normalized_path = os.path.dirname(os.path.dirname(str(isolation))).replace(os.sep, '/') - assert context.format('foo {root:uri:parent:parent}') == f'foo file:{uri_slash_prefix}{normalized_path}' + assert context.format('foo {root:parent:parent:uri}') == f'foo file:{uri_slash_prefix}{normalized_path}' def test_real(self, isolation): context = Context(isolation) @@ -49,12 +59,12 @@ def test_real(self, isolation): def test_real_parent(self, isolation): context = Context(isolation) real_path = os.path.dirname(os.path.realpath(isolation)) - assert context.format('foo {root:real:parent}') == f'foo {real_path}' + assert context.format('foo {root:parent:real}') == f'foo {real_path}' def test_real_parent_parent(self, isolation): context = Context(isolation) real_path = os.path.dirname(os.path.dirname(os.path.realpath(isolation))) - assert context.format('foo {root:real:parent:parent}') == f'foo {real_path}' + assert context.format('foo {root:parent:parent:real}') == f'foo {real_path}' def test_unknown_modifier(self, isolation): context = Context(isolation) @@ -62,6 +72,12 @@ def test_unknown_modifier(self, isolation): with pytest.raises(ValueError, match='Unknown path modifier: bar'): context.format('foo {root:bar}') + def test_too_many_modifiers_after_parent(self, isolation): + context = Context(isolation) + + with pytest.raises(ValueError, match='Expected a single path modifier and instead got: foo, bar, baz'): + context.format('foo {root:parent:foo:bar:baz}') + class TestHome: def test_default(self, isolation):