• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nbiotcloud / ucdp / 18093583429

29 Sep 2025 10:16AM UTC coverage: 90.562% (-6.1%) from 96.647%
18093583429

push

github

web-flow
Merge pull request #132 from nbiotcloud/update-deps

update dependencies

4673 of 5160 relevant lines covered (90.56%)

10.86 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.96
/src/ucdp/pathutil.py
1
#
2
# MIT License
3
#
4
# Copyright (c) 2024-2025 nbiotcloud
5
#
6
# Permission is hereby granted, free of charge, to any person obtaining a copy
7
# of this software and associated documentation files (the "Software"), to deal
8
# in the Software without restriction, including without limitation the rights
9
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
# copies of the Software, and to permit persons to whom the Software is
11
# furnished to do so, subject to the following conditions:
12
#
13
# The above copyright notice and this permission notice shall be included in all
14
# copies or substantial portions of the Software.
15
#
16
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
# SOFTWARE.
23
#
24

25
"""Path Utilities."""
26

27
import os
12✔
28
import os.path
12✔
29
import re
12✔
30
from collections.abc import Iterator
12✔
31
from pathlib import Path
12✔
32

33
_RE_PAT = re.compile(r".*[\*\?\]\[]")
12✔
34
_RE_ENVVAR = re.compile(
12✔
35
    r"^\$(?P<name>(\{(?P<name1>[^\/\\]+)\})|(?P<name0>[^\/\\]+))([\/\\](?P<rem>.*))?$", flags=re.IGNORECASE
36
)
37

38

39
def improved_glob(pattern: Path | str, basedir: Path | None = None) -> Iterator[Path]:
12✔
40
    """
41
    Improved version of pathlib.Path.glob().
42

43
    * Not existing files are not 'globbed-away'
44
    * Aware of Environment Variables
45
    * Output is sorted.
46
    """
47
    basedir = basedir or Path()
12✔
48
    patternstr = str(pattern)
12✔
49
    if _is_pattern(patternstr):
12✔
50
        name, path = startswith_envvar(Path(pattern))
12✔
51
        if name:
12✔
52
            vname = f"${name}"
12✔
53
            base = Path(vname)
12✔
54
            value = Path(os.path.expandvars(vname))
12✔
55
            for sub in sorted(value.glob(str(path))):
12✔
56
                rel = sub.relative_to(value)
12✔
57
                yield base / rel
12✔
58
        else:
59
            yield from sorted(basedir.glob(patternstr))
12✔
60
    else:
61
        yield basedir / pattern
12✔
62

63

64
def improved_resolve(path: Path, basedir: Path | None = None, strict: bool = False, replace_envvars=False) -> Path:
12✔
65
    """
66
    Improved version of pathlib.Path.resolve().
67

68
    * Aware of Environment Variables
69
    """
70
    if replace_envvars:
12✔
71
        path = Path(os.path.expandvars(str(path)))
12✔
72
    elif _RE_ENVVAR.match(str(path)):
12✔
73
        if strict:
12✔
74
            raise FileNotFoundError(path)
12✔
75
        return path
12✔
76

77
    if not path.is_absolute():
12✔
78
        basedir = basedir or Path()
12✔
79
        path = basedir / path
12✔
80
    path = absolute(path)
12✔
81
    if strict:
12✔
82
        path.resolve(strict=True)
12✔
83
    return path
12✔
84

85

86
def use_envvars(path: Path, envvarnames: tuple[str, ...]) -> Path:
12✔
87
    """
88
    Use Environment Variables instead of absolute Paths.
89

90
    Do nothing on relative paths.
91
    """
92
    if not path.is_absolute():
12✔
93
        return path
12✔
94
    for envvarname in envvarnames:
12✔
95
        value = os.getenv(envvarname)
12✔
96
        if not value:
12✔
97
            continue
12✔
98
        try:
12✔
99
            sub = path.relative_to(Path(value))
12✔
100
        except ValueError:
12✔
101
            continue
12✔
102
        return Path(f"${{{envvarname}}}") / sub
12✔
103
    return path
12✔
104

105

106
def startswith_envvar(path: Path, strict: bool = False, barename: bool = False) -> tuple[str | None, Path]:
12✔
107
    """Check if path starts with environment variable."""
108
    mat = _RE_ENVVAR.match(str(path))
12✔
109
    if not mat:
12✔
110
        return None, path
12✔
111
    varname = mat.group("name0") or mat.group("name1")
12✔
112
    path = Path(mat.group("rem") or ".")
12✔
113
    if strict:
12✔
114
        envpath = os.getenv(varname)
12✔
115
        if not envpath or not Path(envpath).exists():
12✔
116
            raise FileNotFoundError(envpath)
12✔
117

118
    if barename:
12✔
119
        return varname, path
12✔
120
    return mat.group("name"), path
12✔
121

122

123
def _is_pattern(pattern: str) -> bool:
12✔
124
    return bool(_RE_PAT.match(pattern))
12✔
125

126

127
def relative(path: Path, base: Path | None = None) -> Path:
12✔
128
    """Relative."""
129
    base = base or absolute(Path())
×
130
    try:
×
131
        return path.relative_to(base)
×
132
    except ValueError:
×
133
        return Path(os.path.relpath(str(path), str(base)))
×
134

135

136
def absolute(path: Path) -> Path:
12✔
137
    """Resolve `..`, but keep symlinks."""
138
    return Path(os.path.abspath(path))  # noqa: PTH100
12✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc