Coverage for pyTooling/GenericPath/__init__.py: 98%
81 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-03 23:02 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-03 23:02 +0000
1# ==================================================================================================================== #
2# _____ _ _ ____ _ ____ _ _ #
3# _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___| ___ _ __ ___ _ __(_) ___| _ \ __ _| |_| |__ #
4# | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | _ / _ \ '_ \ / _ \ '__| |/ __| |_) / _` | __| '_ \ #
5# | |_) | |_| || | (_) | (_) | | | | | | (_| || |_| | __/ | | | __/ | | | (__| __/ (_| | |_| | | | #
6# | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____|\___|_| |_|\___|_| |_|\___|_| \__,_|\__|_| |_| #
7# |_| |___/ |___/ #
8# ==================================================================================================================== #
9# Authors: #
10# Patrick Lehmann #
11# #
12# License: #
13# ==================================================================================================================== #
14# Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany #
15# #
16# Licensed under the Apache License, Version 2.0 (the "License"); #
17# you may not use this file except in compliance with the License. #
18# You may obtain a copy of the License at #
19# #
20# http://www.apache.org/licenses/LICENSE-2.0 #
21# #
22# Unless required by applicable law or agreed to in writing, software #
23# distributed under the License is distributed on an "AS IS" BASIS, #
24# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
25# See the License for the specific language governing permissions and #
26# limitations under the License. #
27# #
28# SPDX-License-Identifier: Apache-2.0 #
29# ==================================================================================================================== #
30#
31"""
32A generic path to derive domain specific path libraries.
34.. seealso::
36 :mod:`pyTooling.GenericPath.URL`
37 |rarr| A URL as a domain-specific path.
38 :mod:`pyTooling.Configuration`
39 |rarr| Path expressions addressing a node in a configuration.
40"""
41from __future__ import annotations
43from typing import ClassVar, Optional as Nullable, Union
45from pyTooling.Common import getFullyQualifiedName
46from pyTooling.Decorators import export
47from pyTooling.MetaClasses import ExtendedType
50@export
51class Base(metaclass=ExtendedType, mixin=True):
52 """Base-mixin-class for all :mod:`pyTooling.GenericPath` path elements."""
54 DELIMITER: ClassVar[str] = "/" #: Path element delimiter sign.
56 _parent: Nullable[Base] #: Reference to the parent object.
58 def __init__(self, parent: Nullable[Base] = None) -> None:
59 """
60 Initialize the base-mixin-class with a parent reference.
62 :param parent: Optional, parent reference.
63 """
64 self._parent = parent
67@export
68class RootMixin(Base, mixin=True):
69 """Mixin-class for root elements in a path system."""
71 def __init__(self) -> None:
72 """
73 Initialize the mixin-class for a root element.
74 """
75 super().__init__(None)
78@export
79class ElementMixin(Base, mixin=True):
80 """Mixin-class for elements in a path system."""
82 _elementName: str #: Name of the path element.
84 def __init__(self, parent: Base, elementName: str) -> None:
85 """
86 Initialize the mixin-class for a path element.
88 :param parent: Optional, reference to a parent path element.
89 :param elementName: Name of the path element.
90 """
91 super().__init__(parent)
93 self._elementName = elementName
95 def __str__(self) -> str:
96 """
97 Return a string representation of this path element.
99 :returns: The element's name.
100 """
101 return self._elementName
104@export
105class PathMixin(metaclass=ExtendedType, mixin=True):
106 """Mixin-class for a path."""
108 ELEMENT_DELIMITER: ClassVar[str] = "/" #: Path element delimiter sign.
109 ROOT_DELIMITER: ClassVar[str] = "/" #: Root element delimiter sign.
110 ELEMENT_TYPE: ClassVar[type[ElementMixin]] #: Type an element of this path flavour has. Every flavour names it.
112 _isAbsolute: bool #: True, if the path is absolute.
113 _elements: list[ElementMixin] #: List of path elements.
115 def __init__(self, elements: list[ElementMixin], isAbsolute: bool) -> None:
116 """
117 Initialize the mixin-class for a path.
119 :param elements: Reference to a parent path element.
120 :param isAbsolute: ``True``, if the path is absolute, otherwise ``False``.
121 """
122 self._isAbsolute = isAbsolute
123 self._elements = elements
125 def __len__(self) -> int:
126 """
127 Returns the number of path elements.
129 :returns: Number of path elements.
130 """
131 return len(self._elements)
133 def __str__(self) -> str:
134 """
135 Return a string representation of this path.
137 :returns: The path's elements, joined by the delimiter, prefixed by the root delimiter if the path is absolute.
138 """
139 result = self.ROOT_DELIMITER if self._isAbsolute else ""
141 if len(self._elements) > 0: 141 ↛ 147line 141 didn't jump to line 147 because the condition on line 141 was always true
142 result = result + str(self._elements[0])
144 for element in self._elements[1:]:
145 result = result + self.ELEMENT_DELIMITER + str(element)
147 return result
149 def __truediv__(self, other: Union[str, PathMixin]) -> PathMixin:
150 """
151 Return this path with another path below it.
153 A trailing delimiter is dropped before appending, so composing ``/api/`` with ``things`` names
154 ``/api/things`` and not an empty element between them. An absolute path names where it starts itself, so it
155 replaces this one rather than being appended - as :rfc:`3986` resolves a reference and :mod:`pathlib` joins a
156 path.
158 :param other: The path to append, as a string to parse or as a path.
159 :returns: A new path, or ``other``, if that one is absolute.
160 :raises TypeError: If parameter 'other' is neither of type :class:`str` nor of type :class:`PathMixin`.
161 """
162 if isinstance(other, str):
163 isAbsolute = other.startswith(self.ROOT_DELIMITER)
164 names = (other[len(self.ROOT_DELIMITER):] if isAbsolute else other).split(self.ELEMENT_DELIMITER)
165 elif isinstance(other, PathMixin):
166 isAbsolute = other._isAbsolute
167 names = [str(element) for element in other._elements]
168 else:
169 ex = TypeError("Second operand is not supported by / operator.")
170 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
171 ex.add_note(f"Supported types for second operand: 'str' or '{getFullyQualifiedName(self)}'.")
172 raise ex
174 if isAbsolute:
175 path = self
176 elements = []
177 else:
178 path = self.WithoutTrailingDelimiter()
179 elements = list(path._elements)
181 parent = elements[-1] if len(elements) > 0 else None
182 for name in names:
183 elements.append(parent := self.ELEMENT_TYPE(parent, name))
185 return self.__class__(elements, isAbsolute or path._isAbsolute)
187 def WithoutTrailingDelimiter(self) -> PathMixin:
188 """
189 Return a path that doesn't end in :attr:`ELEMENT_DELIMITER`.
191 A trailing delimiter is an empty last element, so ``/api/v3/`` and ``/api/v3`` are different paths although
192 they usually name the same thing. A path that something is appended to wants the latter, or the composition
193 yields two delimiters in a row.
195 Only one trailing delimiter is removed: a path ending in two of them names an empty element and then another,
196 which isn't the same as naming neither.
198 :returns: A new path without a trailing delimiter, or this path, if it has none.
199 """
200 if (elementCount := len(self._elements)) == 0 or str(self._elements[-1]) != "":
201 return self
202 elif elementCount > 1:
203 return self.__class__(self._elements[:-1], self._isAbsolute)
205 # A path of nothing but the empty element is the root: it has no element to drop, so it stops being absolute.
206 # The same path that isn't absolute is the empty path, which has no trailing delimiter to begin with.
207 return self if not self._isAbsolute else self.__class__(self._elements, False)
209 @classmethod
210 def Parse(cls, path: str, root: Nullable[RootMixin] = None) -> PathMixin:
211 """
212 Parses a string representation of a path and returns a path instance.
214 The path and its elements are of this flavour's types - the class this is called on, and the
215 :attr:`ELEMENT_TYPE` it names.
217 :param path: Path to be parsed.
218 :param root: Optional, root element the parsed path is relative to. Default: no root.
219 :returns: A path instance of this class.
220 """
221 if path.startswith(cls.ROOT_DELIMITER):
222 isAbsolute = True
223 path = path[len(cls.ROOT_DELIMITER):]
224 else:
225 isAbsolute = False
227 parent = root
228 elements = []
229 for part in path.split(cls.ELEMENT_DELIMITER):
230 elements.append(parent := cls.ELEMENT_TYPE(parent, part))
232 return cls(elements, isAbsolute)
235@export
236class SystemMixin(metaclass=ExtendedType, mixin=True):
237 """Mixin-class for a path system."""