tstr 0.2.0__tar.gz → 0.3.1.dev1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. tstr-0.3.1.dev1/.github/workflows/build.yml +58 -0
  2. {tstr-0.2.0 → tstr-0.3.1.dev1}/.github/workflows/codeql.yml +4 -4
  3. {tstr-0.2.0 → tstr-0.3.1.dev1}/.github/workflows/testing.yaml +13 -4
  4. {tstr-0.2.0 → tstr-0.3.1.dev1}/.vscode/settings.json +5 -1
  5. {tstr-0.2.0 → tstr-0.3.1.dev1}/PKG-INFO +20 -64
  6. {tstr-0.2.0 → tstr-0.3.1.dev1}/README.md +18 -62
  7. {tstr-0.2.0 → tstr-0.3.1.dev1}/pyproject.toml +6 -0
  8. {tstr-0.2.0 → tstr-0.3.1.dev1}/src/tstr/__init__.py +13 -8
  9. {tstr-0.2.0 → tstr-0.3.1.dev1}/src/tstr/_compat.py +8 -2
  10. tstr-0.3.1.dev1/src/tstr/_interpolation_tools.py +141 -0
  11. {tstr-0.2.0 → tstr-0.3.1.dev1}/src/tstr/_template.py +3 -3
  12. tstr-0.3.1.dev1/src/tstr/_template.pyi +20 -0
  13. tstr-0.2.0/src/tstr/_utils.py → tstr-0.3.1.dev1/src/tstr/_template_tools.py +169 -104
  14. tstr-0.3.1.dev1/src/tstr/ext/__init__.py +0 -0
  15. {tstr-0.2.0/src/tstr → tstr-0.3.1.dev1/src/tstr/ext}/_html.py +17 -9
  16. {tstr-0.2.0/src/tstr → tstr-0.3.1.dev1/src/tstr/ext}/_logging.py +42 -11
  17. {tstr-0.2.0/src/tstr → tstr-0.3.1.dev1/src/tstr/ext}/_sqlite.py +32 -12
  18. tstr-0.3.1.dev1/src/tstr/ext/py.typed +0 -0
  19. tstr-0.3.1.dev1/src/tstr/py.typed +0 -0
  20. {tstr-0.2.0 → tstr-0.3.1.dev1}/tests/_compat1.py +76 -8
  21. {tstr-0.2.0 → tstr-0.3.1.dev1}/tests/_compat2.py +31 -51
  22. {tstr-0.2.0 → tstr-0.3.1.dev1}/tests/test_generate_template1.py +88 -16
  23. {tstr-0.2.0 → tstr-0.3.1.dev1}/tests/test_generate_template2.py +51 -41
  24. {tstr-0.2.0 → tstr-0.3.1.dev1}/tests/test_html.py +11 -2
  25. {tstr-0.2.0 → tstr-0.3.1.dev1}/tests/test_logging.py +6 -3
  26. tstr-0.3.1.dev1/tests/test_sqlite.py +37 -0
  27. tstr-0.3.1.dev1/tests/test_utils.py +819 -0
  28. tstr-0.3.1.dev1/uv.lock +227 -0
  29. tstr-0.2.0/.devcontainer/Dockerfile +0 -20
  30. tstr-0.2.0/.devcontainer/devcontainer.json +0 -44
  31. tstr-0.2.0/.github/workflows/build.yml +0 -61
  32. tstr-0.2.0/docs/api.md +0 -125
  33. tstr-0.2.0/src/tstr/_template.pyi +0 -72
  34. tstr-0.2.0/tests/test_sqlite.py +0 -16
  35. tstr-0.2.0/tests/test_utils.py +0 -157
  36. tstr-0.2.0/uv.lock +0 -212
  37. {tstr-0.2.0 → tstr-0.3.1.dev1}/.github/FUNDING.yml +0 -0
  38. {tstr-0.2.0 → tstr-0.3.1.dev1}/.gitignore +0 -0
  39. {tstr-0.2.0 → tstr-0.3.1.dev1}/LICENSE +0 -0
  40. {tstr-0.2.0 → tstr-0.3.1.dev1}/tests/_support.py +0 -0
  41. {tstr-0.2.0 → tstr-0.3.1.dev1}/tests/test_compat.py +0 -0
@@ -0,0 +1,58 @@
1
+ # The following build script copied from hatch/.github/workflows/build-hatchling.yml (MIT License)
2
+ name: build a package
3
+
4
+ on:
5
+ push:
6
+ tags:
7
+ - v*
8
+
9
+ env:
10
+ PYTHON_VERSION: "3.12"
11
+
12
+ jobs:
13
+ build:
14
+ name: Build wheels and source distribution
15
+ runs-on: ubuntu-latest
16
+
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+
20
+ - name: Set up Python ${{ env.PYTHON_VERSION }}
21
+ uses: actions/setup-python@v5
22
+ with:
23
+ python-version: ${{ env.PYTHON_VERSION }}
24
+
25
+ - name: Install UV
26
+ uses: astral-sh/setup-uv@v3
27
+
28
+ - name: Install build dependencies
29
+ run: uv pip install --system --upgrade build
30
+
31
+ - name: Build source distribution
32
+ run: python -m build .
33
+
34
+ - uses: actions/upload-artifact@v4
35
+ with:
36
+ name: artifacts
37
+ path: dist
38
+ if-no-files-found: error
39
+
40
+ publish:
41
+ name: Publish release
42
+ needs:
43
+ - build
44
+ runs-on: ubuntu-latest
45
+
46
+ permissions:
47
+ id-token: write
48
+
49
+ steps:
50
+ - uses: actions/download-artifact@v4
51
+ with:
52
+ name: artifacts
53
+ path: dist
54
+
55
+ - name: Push build artifacts to PyPI
56
+ uses: pypa/gh-action-pypi-publish@v1.12.3
57
+ with:
58
+ skip-existing: true
@@ -41,11 +41,11 @@ jobs:
41
41
 
42
42
  steps:
43
43
  - name: Checkout repository
44
- uses: actions/checkout@v3
44
+ uses: actions/checkout@v5
45
45
 
46
46
  # Initializes the CodeQL tools for scanning.
47
47
  - name: Initialize CodeQL
48
- uses: github/codeql-action/init@v2
48
+ uses: github/codeql-action/init@v3
49
49
  with:
50
50
  languages: ${{ matrix.language }}
51
51
  # If you wish to specify custom queries, you can do so here or in a config file.
@@ -59,7 +59,7 @@ jobs:
59
59
  # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
60
60
  # If this step fails, then you should remove it and run the build manually (see below)
61
61
  - name: Autobuild
62
- uses: github/codeql-action/autobuild@v2
62
+ uses: github/codeql-action/autobuild@v3
63
63
 
64
64
  # ℹ️ Command-line programs to run using the OS shell.
65
65
  # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -72,6 +72,6 @@ jobs:
72
72
  # ./location_of_script_within_repo/buildscript.sh
73
73
 
74
74
  - name: Perform CodeQL Analysis
75
- uses: github/codeql-action/analyze@v2
75
+ uses: github/codeql-action/analyze@v3
76
76
  with:
77
77
  category: "/language:${{matrix.language}}"
@@ -12,22 +12,31 @@ jobs:
12
12
  matrix:
13
13
  os: [ubuntu-latest]
14
14
  python-version: ['3.10', '3.11', '3.12', '3.13', '3.14-dev']
15
+
15
16
  runs-on: ${{ matrix.os }}
16
17
  steps:
17
18
  - uses: actions/checkout@v4
19
+
18
20
  - name: Set up Python
19
21
  uses: actions/setup-python@v5
20
22
  with:
21
23
  python-version: ${{ matrix.python-version }}
24
+
25
+ - name: Install UV
26
+ uses: astral-sh/setup-uv@v3
27
+
22
28
  - name: Install dependencies
23
29
  run: |
24
- python -m pip install pytest pytest-cov
25
- python -m pip install -e .
30
+ # python -m pip install pytest pytest-cov
31
+ # python -m pip install -e .
32
+ uv sync
33
+
26
34
  - name: Test with pytest
27
35
  run: |
28
36
  python -m pytest --cov --cov-report lcov
29
- - name: Coveralls
30
- uses: coverallsapp/github-action@master
37
+
38
+ - name: Upload coverage information to Coveralls
39
+ uses: coverallsapp/github-action@v2
31
40
  with:
32
41
  github-token: ${{ secrets.GITHUB_TOKEN }}
33
42
  path-to-lcov: coverage.lcov
@@ -1,6 +1,8 @@
1
1
  {
2
2
  "cSpell.words": [
3
+ "dedented",
3
4
  "fetchone",
5
+ "fstring",
4
6
  "interp",
5
7
  "intp",
6
8
  "intrp",
@@ -8,7 +10,9 @@
8
10
  "renderable",
9
11
  "repr",
10
12
  "templatelib",
11
- "tstr"
13
+ "tstr",
14
+ "tstring",
15
+ "tstrings"
12
16
  ],
13
17
  "python.analysis.typeCheckingMode": "standard"
14
18
  }
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tstr
3
- Version: 0.2.0
3
+ Version: 0.3.1.dev1
4
4
  Summary: Template string utilities and backports
5
5
  Project-URL: Repository, https://github.com/ilotoki0804/tstr
6
6
  Author-email: ilotoki0804 <ilotoki0804@gmail.com>
7
7
  License-Expression: Apache-2.0
8
8
  License-File: LICENSE
9
- Keywords: backport,string,template,utility
9
+ Keywords: backport,pep 750,pep-750,pep750,string,t-string,template,template string,tstring,utility
10
10
  Classifier: License :: OSI Approved :: Apache Software License
11
11
  Classifier: Operating System :: OS Independent
12
12
  Classifier: Programming Language :: Python :: 3
@@ -43,70 +43,23 @@ pip install tstr
43
43
  - `render` (alias: `f`): Render a template to a string, mimicking f-string behavior.
44
44
  - `generate_template` (alias: `t`): Create a Template object from a string and context.
45
45
  This function is especially useful on Python versions that do not support template strings natively.
46
- - `bind`: Apply a function to all interpolations in a template.
46
+ - `bind`: Apply a function to all interpolations and join all the parts.
47
47
  - `binder`: Decorator to create template processors from an interpolation processor.
48
- - `normalize`: Convert an interpolation to its value, preserving type when possible.
49
- - `normalize_str`: Convert an interpolation to a string.
50
- - `convert`: Apply f-string-style conversion to a value.
48
+ - `normalize` / `normalize_str`: Apply conversion and format to value.
49
+ - `convert`: Apply conversion to a value.
51
50
  - `template_eq`: Check if two templates are equivalent.
51
+ - `interpolation_replace`: Create a new `Interpolation` by selectively replacing attributes of an existing one.
52
+ - `dedent`: `textwrap.dedent` for template strings
53
+ - `template_from_parts`: Construct template strings from iterable
52
54
 
53
- Experimental applications:
54
-
55
- - `render_html`: Render templates with HTML escaping.
56
- - `execute`: Safely execute SQL with templates, preventing injection.
57
- - `TemplateFormatter`: Enable Python's logging module to accept template strings.
58
-
59
- Below are usage examples for each function. For more details, see the [API documentation](/docs/api.md).
60
-
61
- ```python
62
- from tstr import Template, Interpolation, generate_template, render, bind, binder, f, normalize, normalize_str, convert, template_eq
63
-
64
- # Rendering templates
65
- x = 12
66
- template = t"Value: {x}"
67
- print(render(template)) # "Value: 12"
68
- print(f(template)) # "Value: 12"
69
-
70
- # Generating templates
71
- template = generate_template("Hello, {name}!", {"name": "Alice"}) # with explicit context
72
- print(f(template)) # "Hello, Alice!"
73
-
74
- name = "Bob"
75
- template = generate_template("Nice to meet you, {name}!") # without explicit context
76
- print(f(template)) # "Nice to meet you, Bob!"
77
-
78
- template = t("Nice to meet you, {name}!") # with an alias `t`
79
- print(f(template)) # "Nice to meet you, Bob!"
80
-
81
- # Binding all interpolations
82
- def double(i: Interpolation):
83
- return str(i.value * 2)
84
- n = 10
85
- template = t"Double: {n}"
86
- print(bind(template, double)) # "Double: 20"
87
-
88
- @binder
89
- def upper(i):
90
- return normalize_str(i).upper()
91
- name = 'bob'
92
- template = t"Hi, {name}!"
93
- print(upper(template)) # "Hi, BOB!"
94
-
95
- # Applying conversion and formatting
96
- age = 20
97
- template = t"Age: {age:04d}"
98
- intp = template.interpolations[0]
99
- print(normalize(intp)) # "0020"
100
-
101
- # Applying conversion
102
- print(convert(42, "r")) # e.g., "42"
103
-
104
- # Template equivalence
105
- x = 123
106
- t1 = t"A: {x}"
107
- t2 = t"A: {x}"
108
- print(template_eq(t1, t2)) # True
109
- ```
55
+ This library also provides several useful extensions where template strings can be effectively utilized.
56
+ These extensions are available in the `tstr.ext` submodule, and below is a list with brief descriptions:
57
+
58
+ - `ext._html`: Render templates with HTML escaping.
59
+ - `ext._sqlite`: Safely execute SQL with templates, preventing SQL injection attacks.
60
+ - `ext._logging`: Enable Python's logging module to accept template strings.
61
+
62
+ For more details, see the [API documentation](/docs/api.md).
110
63
 
111
64
  ## Compatibility
112
65
 
@@ -117,15 +70,18 @@ print(template_eq(t1, t2)) # True
117
70
 
118
71
  Use the `TEMPLATE_STRING_SUPPORTED` constant to check if template strings are natively supported in your Python version.
119
72
 
73
+ For details on how the compatible backport of template string works and what similarities and differences it has with native template strings, see the [compatible template strings](/docs/compat.md) documentation.
74
+
120
75
  # Contributing
121
76
 
122
77
  This project welcomes contributions of all kinds from anyone willing to help improve it! Whether you're fixing a typo in documentation, reporting a bug, proposing a new feature, or implementing code changes - every contribution matters and is highly appreciated.
123
78
 
124
79
  ## Releases
125
80
 
81
+ * 0.3.0: Revamp various things
126
82
  * 0.2.0: Rename html_render to render_html, add `_logging` module, fix various bugs and improve documentation
127
83
  * 0.1.1.post1: Initial release
128
84
 
129
85
  ## License
130
86
 
131
- Apache License 2.0
87
+ This library is licensed under the Apache License 2.0.
@@ -23,70 +23,23 @@ pip install tstr
23
23
  - `render` (alias: `f`): Render a template to a string, mimicking f-string behavior.
24
24
  - `generate_template` (alias: `t`): Create a Template object from a string and context.
25
25
  This function is especially useful on Python versions that do not support template strings natively.
26
- - `bind`: Apply a function to all interpolations in a template.
26
+ - `bind`: Apply a function to all interpolations and join all the parts.
27
27
  - `binder`: Decorator to create template processors from an interpolation processor.
28
- - `normalize`: Convert an interpolation to its value, preserving type when possible.
29
- - `normalize_str`: Convert an interpolation to a string.
30
- - `convert`: Apply f-string-style conversion to a value.
28
+ - `normalize` / `normalize_str`: Apply conversion and format to value.
29
+ - `convert`: Apply conversion to a value.
31
30
  - `template_eq`: Check if two templates are equivalent.
31
+ - `interpolation_replace`: Create a new `Interpolation` by selectively replacing attributes of an existing one.
32
+ - `dedent`: `textwrap.dedent` for template strings
33
+ - `template_from_parts`: Construct template strings from iterable
32
34
 
33
- Experimental applications:
34
-
35
- - `render_html`: Render templates with HTML escaping.
36
- - `execute`: Safely execute SQL with templates, preventing injection.
37
- - `TemplateFormatter`: Enable Python's logging module to accept template strings.
38
-
39
- Below are usage examples for each function. For more details, see the [API documentation](/docs/api.md).
40
-
41
- ```python
42
- from tstr import Template, Interpolation, generate_template, render, bind, binder, f, normalize, normalize_str, convert, template_eq
43
-
44
- # Rendering templates
45
- x = 12
46
- template = t"Value: {x}"
47
- print(render(template)) # "Value: 12"
48
- print(f(template)) # "Value: 12"
49
-
50
- # Generating templates
51
- template = generate_template("Hello, {name}!", {"name": "Alice"}) # with explicit context
52
- print(f(template)) # "Hello, Alice!"
53
-
54
- name = "Bob"
55
- template = generate_template("Nice to meet you, {name}!") # without explicit context
56
- print(f(template)) # "Nice to meet you, Bob!"
57
-
58
- template = t("Nice to meet you, {name}!") # with an alias `t`
59
- print(f(template)) # "Nice to meet you, Bob!"
60
-
61
- # Binding all interpolations
62
- def double(i: Interpolation):
63
- return str(i.value * 2)
64
- n = 10
65
- template = t"Double: {n}"
66
- print(bind(template, double)) # "Double: 20"
67
-
68
- @binder
69
- def upper(i):
70
- return normalize_str(i).upper()
71
- name = 'bob'
72
- template = t"Hi, {name}!"
73
- print(upper(template)) # "Hi, BOB!"
74
-
75
- # Applying conversion and formatting
76
- age = 20
77
- template = t"Age: {age:04d}"
78
- intp = template.interpolations[0]
79
- print(normalize(intp)) # "0020"
80
-
81
- # Applying conversion
82
- print(convert(42, "r")) # e.g., "42"
83
-
84
- # Template equivalence
85
- x = 123
86
- t1 = t"A: {x}"
87
- t2 = t"A: {x}"
88
- print(template_eq(t1, t2)) # True
89
- ```
35
+ This library also provides several useful extensions where template strings can be effectively utilized.
36
+ These extensions are available in the `tstr.ext` submodule, and below is a list with brief descriptions:
37
+
38
+ - `ext._html`: Render templates with HTML escaping.
39
+ - `ext._sqlite`: Safely execute SQL with templates, preventing SQL injection attacks.
40
+ - `ext._logging`: Enable Python's logging module to accept template strings.
41
+
42
+ For more details, see the [API documentation](/docs/api.md).
90
43
 
91
44
  ## Compatibility
92
45
 
@@ -97,15 +50,18 @@ print(template_eq(t1, t2)) # True
97
50
 
98
51
  Use the `TEMPLATE_STRING_SUPPORTED` constant to check if template strings are natively supported in your Python version.
99
52
 
53
+ For details on how the compatible backport of template string works and what similarities and differences it has with native template strings, see the [compatible template strings](/docs/compat.md) documentation.
54
+
100
55
  # Contributing
101
56
 
102
57
  This project welcomes contributions of all kinds from anyone willing to help improve it! Whether you're fixing a typo in documentation, reporting a bug, proposing a new feature, or implementing code changes - every contribution matters and is highly appreciated.
103
58
 
104
59
  ## Releases
105
60
 
61
+ * 0.3.0: Revamp various things
106
62
  * 0.2.0: Rename html_render to render_html, add `_logging` module, fix various bugs and improve documentation
107
63
  * 0.1.1.post1: Initial release
108
64
 
109
65
  ## License
110
66
 
111
- Apache License 2.0
67
+ This library is licensed under the Apache License 2.0.
@@ -10,6 +10,12 @@ keywords = [
10
10
  "string",
11
11
  "utility",
12
12
  "backport",
13
+ "template string",
14
+ "t-string",
15
+ "tstring",
16
+ "pep-750",
17
+ "pep750",
18
+ "pep 750",
13
19
  ]
14
20
  dependencies = []
15
21
  classifiers = [
@@ -1,3 +1,9 @@
1
+ from ._interpolation_tools import (
2
+ convert,
3
+ interpolation_replace,
4
+ normalize,
5
+ normalize_str,
6
+ )
1
7
  from ._template import (
2
8
  TEMPLATE_STRING_SUPPORTED,
3
9
  Conversion,
@@ -5,15 +11,13 @@ from ._template import (
5
11
  StringOrTemplate,
6
12
  Template,
7
13
  )
8
- from ._utils import (
9
- CONVERTERS,
10
- TemplateGenerationError,
14
+ from ._template_tools import (
11
15
  bind,
12
16
  binder,
13
- convert,
17
+ dedent,
14
18
  f,
19
+ template_from_parts,
15
20
  generate_template,
16
- normalize,
17
21
  normalize_str,
18
22
  render,
19
23
  t,
@@ -21,7 +25,6 @@ from ._utils import (
21
25
  )
22
26
 
23
27
  __all__ = [
24
- "CONVERTERS",
25
28
  "bind",
26
29
  "binder",
27
30
  "f",
@@ -34,9 +37,11 @@ __all__ = [
34
37
  "Conversion",
35
38
  "generate_template",
36
39
  "t",
37
- "TemplateGenerationError",
38
40
  "TEMPLATE_STRING_SUPPORTED",
39
41
  "template_eq",
40
42
  "StringOrTemplate",
43
+ "interpolation_replace",
44
+ "template_from_parts",
45
+ "dedent",
41
46
  ]
42
- __version__ = "0.2.0"
47
+ __version__ = "0.3.1.dev1"
@@ -3,7 +3,10 @@ from __future__ import annotations
3
3
  import typing
4
4
  from itertools import zip_longest
5
5
 
6
+ __all__ = ["Template", "Interpolation"]
6
7
 
8
+
9
+ @typing.final
7
10
  class Template:
8
11
  __strings: tuple[str, ...]
9
12
  __interpolations: tuple[Interpolation, ...]
@@ -80,7 +83,8 @@ class Template:
80
83
 
81
84
  def __add__(self, other: str | Template) -> Template:
82
85
  if isinstance(other, str):
83
- return Template(*self, other)
86
+ raise TypeError(
87
+ 'can only concatenate tstr.Template (not "str") to tstr.Template')
84
88
  elif isinstance(other, Template):
85
89
  return Template(*self, *other)
86
90
  else:
@@ -88,7 +92,8 @@ class Template:
88
92
 
89
93
  def __radd__(self, other: str | Template) -> Template:
90
94
  if isinstance(other, str):
91
- return Template(other, *self)
95
+ raise TypeError(
96
+ 'can only concatenate str (not "tstr.Template") to str')
92
97
  elif isinstance(other, Template):
93
98
  return Template(*other, *self)
94
99
  else:
@@ -98,6 +103,7 @@ class Template:
98
103
  return f"Template(strings={self.strings!r}, interpolations={self.interpolations!r})"
99
104
 
100
105
 
106
+ @typing.final
101
107
  class Interpolation:
102
108
  __match_args__ = ("value", "expression", "conversion", "format_spec")
103
109
  __value: object
@@ -0,0 +1,141 @@
1
+ from __future__ import annotations
2
+
3
+ import typing
4
+
5
+ from ._template import Conversion, Interpolation
6
+
7
+ __all__ = [
8
+ "convert",
9
+ "normalize",
10
+ "normalize_str",
11
+ "interpolation_replace",
12
+ ]
13
+
14
+ T = typing.TypeVar("T")
15
+
16
+
17
+ @typing.overload
18
+ def convert(
19
+ value: typing.Any,
20
+ conversion: Conversion,
21
+ ) -> str: ...
22
+
23
+ @typing.overload
24
+ def convert(
25
+ value: typing.Any,
26
+ conversion: str,
27
+ ) -> str: ...
28
+
29
+ @typing.overload
30
+ def convert(
31
+ value: T,
32
+ conversion: Conversion | None,
33
+ ) -> T | str: ...
34
+
35
+ @typing.overload
36
+ def convert(
37
+ value: T,
38
+ conversion: str | None,
39
+ ) -> T | str: ...
40
+
41
+
42
+ def convert(
43
+ value: T,
44
+ conversion: str | None,
45
+ ) -> T | str:
46
+ """
47
+ Applies a conversion to a value, similar to how f-strings handle conversions.
48
+
49
+ Args:
50
+ value (T): The value to convert, typically from an Interpolation.value.
51
+ conversion (Conversion | None): The conversion specifier ('a', 'r', or 's'), or None.
52
+
53
+ Returns:
54
+ T | str: The value converted according to the specified conversion;
55
+ if 'conversion' is None, returns the original value unchanged.
56
+ """
57
+ if conversion is None:
58
+ return value
59
+ if conversion == "s":
60
+ return str(value)
61
+ if conversion == "r":
62
+ return repr(value)
63
+ if conversion == "a":
64
+ return ascii(value)
65
+ raise ValueError(f"Invalid conversion: {conversion}")
66
+
67
+
68
+ def normalize(interp: Interpolation) -> str | object:
69
+ """
70
+ Normalizes a PEP 750 Interpolation, preserving its type when possible.
71
+
72
+ This is a more flexible version of normalize_str() that preserves the original
73
+ value's type when no conversion is specified.
74
+
75
+ If neither a conversion nor a format spec is specified, the original value
76
+ is returned without any modification, ensuring that the value's type is preserved.
77
+
78
+ Args:
79
+ interp (Interpolation): The interpolation to normalize.
80
+
81
+ Returns:
82
+ str | object: The normalized string if conversion or format spec is specified, otherwise
83
+ the original value.
84
+ """
85
+ if interp.conversion or interp.format_spec:
86
+ return normalize_str(interp)
87
+ else:
88
+ return interp.value
89
+
90
+
91
+ def normalize_str(interp: Interpolation) -> str:
92
+ """
93
+ Normalizes a PEP 750 Interpolation to a formatted string.
94
+
95
+ This processes an Interpolation object similarly to how f-strings process
96
+ interpolated expressions: it applies conversion and format specification.
97
+ Unlike normalize(), this always returns a string.
98
+
99
+ Args:
100
+ interp (Interpolation): The interpolation to normalize.
101
+
102
+ Returns:
103
+ str: The formatted string representation of the interpolation.
104
+ """
105
+ converted = convert(interp.value, interp.conversion)
106
+ return format(converted, interp.format_spec)
107
+
108
+
109
+ _NOTSET = object()
110
+
111
+
112
+ def interpolation_replace(
113
+ interp: Interpolation,
114
+ *,
115
+ value: object = _NOTSET,
116
+ expression: str = _NOTSET, # type: ignore
117
+ conversion: typing.Literal["a", "r", "s"] | None = _NOTSET, # type: ignore
118
+ format_spec: str = _NOTSET, # type: ignore
119
+ ) -> Interpolation:
120
+ """
121
+ Creates a new Interpolation by selectively replacing attributes of an existing one.
122
+
123
+ This function allows you to create a modified copy of an Interpolation object
124
+ by specifying which attributes to replace. Any attribute not explicitly provided
125
+ will retain its original value from the input interpolation.
126
+
127
+ Args:
128
+ interp (Interpolation): The original interpolation object.
129
+ value (object, optional): New value to use instead of the original.
130
+ expression (str, optional): New expression to use instead of the original.
131
+ conversion (Literal["a", "r", "s"] | None, optional): New conversion to use.
132
+ format_spec (str, optional): New format specification to use.
133
+
134
+ Returns:
135
+ Interpolation: A new Interpolation with the specified replacements.
136
+ """
137
+ value = interp.value if value is _NOTSET else value
138
+ expression = interp.expression if expression is _NOTSET else expression
139
+ conversion = interp.conversion if conversion is _NOTSET else conversion
140
+ format_spec = interp.format_spec if format_spec is _NOTSET else format_spec
141
+ return Interpolation(value, expression, conversion, format_spec) # type: ignore
@@ -8,12 +8,12 @@ Conversion: typing.TypeAlias = typing.Literal["a", "r", "s"]
8
8
 
9
9
  try:
10
10
  from string.templatelib import Interpolation, Template # type: ignore
11
+
12
+ TEMPLATE_STRING_SUPPORTED = True
13
+ StringOrTemplate: typing.TypeAlias = typing.LiteralString | Template # type: ignore
11
14
  except Exception:
12
15
  # Fallback to compatible implementation if template strings are not supported
13
16
  from ._compat import Interpolation, Template
14
17
 
15
18
  TEMPLATE_STRING_SUPPORTED = False
16
19
  StringOrTemplate: typing.TypeAlias = str | Template # type: ignore
17
- else:
18
- TEMPLATE_STRING_SUPPORTED = True
19
- StringOrTemplate: typing.TypeAlias = typing.LiteralString | Template # type: ignore
@@ -0,0 +1,20 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ import typing
5
+
6
+ __all__ = ["Template", "Interpolation", "Conversion"]
7
+
8
+ Conversion: typing.TypeAlias = typing.Literal["a", "r", "s"]
9
+
10
+ if sys.version_info >= (3, 14):
11
+ TEMPLATE_STRING_SUPPORTED = True
12
+ StringOrTemplate: typing.TypeAlias = typing.LiteralString | Template
13
+
14
+ from string.templatelib import Interpolation, Template
15
+
16
+ else:
17
+ TEMPLATE_STRING_SUPPORTED = False
18
+ StringOrTemplate: typing.TypeAlias = str | Template
19
+
20
+ from ._compat import Interpolation, Template