mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-08-22 06:13:25 -07:00
Update importlib-metadata==7.1.0
This commit is contained in:
parent
eef91fcae6
commit
a5c17287ea
11 changed files with 518 additions and 192 deletions
|
@ -1,10 +1,14 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import abc
|
||||
import csv
|
||||
import sys
|
||||
import json
|
||||
import zipp
|
||||
import email
|
||||
import types
|
||||
import inspect
|
||||
import pathlib
|
||||
import operator
|
||||
import textwrap
|
||||
|
@ -14,12 +18,12 @@ import itertools
|
|||
import posixpath
|
||||
import collections
|
||||
|
||||
from . import _adapters, _meta, _py39compat
|
||||
from . import _adapters, _meta
|
||||
from .compat import py39
|
||||
from ._collections import FreezableDefaultDict, Pair
|
||||
from ._compat import (
|
||||
NullFinder,
|
||||
install,
|
||||
pypy_partial,
|
||||
)
|
||||
from ._functools import method_cache, pass_none
|
||||
from ._itertools import always_iterable, unique_everseen
|
||||
|
@ -29,8 +33,7 @@ from contextlib import suppress
|
|||
from importlib import import_module
|
||||
from importlib.abc import MetaPathFinder
|
||||
from itertools import starmap
|
||||
from typing import List, Mapping, Optional
|
||||
|
||||
from typing import Any, Iterable, List, Mapping, Match, Optional, Set, cast
|
||||
|
||||
__all__ = [
|
||||
'Distribution',
|
||||
|
@ -51,11 +54,11 @@ __all__ = [
|
|||
class PackageNotFoundError(ModuleNotFoundError):
|
||||
"""The package was not found."""
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return f"No package metadata was found for {self.name}"
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
def name(self) -> str: # type: ignore[override]
|
||||
(name,) = self.args
|
||||
return name
|
||||
|
||||
|
@ -121,38 +124,11 @@ class Sectioned:
|
|||
yield Pair(name, value)
|
||||
|
||||
@staticmethod
|
||||
def valid(line):
|
||||
def valid(line: str):
|
||||
return line and not line.startswith('#')
|
||||
|
||||
|
||||
class DeprecatedTuple:
|
||||
"""
|
||||
Provide subscript item access for backward compatibility.
|
||||
|
||||
>>> recwarn = getfixture('recwarn')
|
||||
>>> ep = EntryPoint(name='name', value='value', group='group')
|
||||
>>> ep[:]
|
||||
('name', 'value', 'group')
|
||||
>>> ep[0]
|
||||
'name'
|
||||
>>> len(recwarn)
|
||||
1
|
||||
"""
|
||||
|
||||
# Do not remove prior to 2023-05-01 or Python 3.13
|
||||
_warn = functools.partial(
|
||||
warnings.warn,
|
||||
"EntryPoint tuple interface is deprecated. Access members by name.",
|
||||
DeprecationWarning,
|
||||
stacklevel=pypy_partial(2),
|
||||
)
|
||||
|
||||
def __getitem__(self, item):
|
||||
self._warn()
|
||||
return self._key()[item]
|
||||
|
||||
|
||||
class EntryPoint(DeprecatedTuple):
|
||||
class EntryPoint:
|
||||
"""An entry point as defined by Python packaging conventions.
|
||||
|
||||
See `the packaging docs on entry points
|
||||
|
@ -194,34 +170,37 @@ class EntryPoint(DeprecatedTuple):
|
|||
value: str
|
||||
group: str
|
||||
|
||||
dist: Optional['Distribution'] = None
|
||||
dist: Optional[Distribution] = None
|
||||
|
||||
def __init__(self, name, value, group):
|
||||
def __init__(self, name: str, value: str, group: str) -> None:
|
||||
vars(self).update(name=name, value=value, group=group)
|
||||
|
||||
def load(self):
|
||||
def load(self) -> Any:
|
||||
"""Load the entry point from its definition. If only a module
|
||||
is indicated by the value, return that module. Otherwise,
|
||||
return the named object.
|
||||
"""
|
||||
match = self.pattern.match(self.value)
|
||||
match = cast(Match, self.pattern.match(self.value))
|
||||
module = import_module(match.group('module'))
|
||||
attrs = filter(None, (match.group('attr') or '').split('.'))
|
||||
return functools.reduce(getattr, attrs, module)
|
||||
|
||||
@property
|
||||
def module(self):
|
||||
def module(self) -> str:
|
||||
match = self.pattern.match(self.value)
|
||||
assert match is not None
|
||||
return match.group('module')
|
||||
|
||||
@property
|
||||
def attr(self):
|
||||
def attr(self) -> str:
|
||||
match = self.pattern.match(self.value)
|
||||
assert match is not None
|
||||
return match.group('attr')
|
||||
|
||||
@property
|
||||
def extras(self):
|
||||
def extras(self) -> List[str]:
|
||||
match = self.pattern.match(self.value)
|
||||
assert match is not None
|
||||
return re.findall(r'\w+', match.group('extras') or '')
|
||||
|
||||
def _for(self, dist):
|
||||
|
@ -269,7 +248,7 @@ class EntryPoint(DeprecatedTuple):
|
|||
f'group={self.group!r})'
|
||||
)
|
||||
|
||||
def __hash__(self):
|
||||
def __hash__(self) -> int:
|
||||
return hash(self._key())
|
||||
|
||||
|
||||
|
@ -280,7 +259,7 @@ class EntryPoints(tuple):
|
|||
|
||||
__slots__ = ()
|
||||
|
||||
def __getitem__(self, name): # -> EntryPoint:
|
||||
def __getitem__(self, name: str) -> EntryPoint: # type: ignore[override]
|
||||
"""
|
||||
Get the EntryPoint in self matching name.
|
||||
"""
|
||||
|
@ -289,22 +268,29 @@ class EntryPoints(tuple):
|
|||
except StopIteration:
|
||||
raise KeyError(name)
|
||||
|
||||
def select(self, **params):
|
||||
def __repr__(self):
|
||||
"""
|
||||
Repr with classname and tuple constructor to
|
||||
signal that we deviate from regular tuple behavior.
|
||||
"""
|
||||
return '%s(%r)' % (self.__class__.__name__, tuple(self))
|
||||
|
||||
def select(self, **params) -> EntryPoints:
|
||||
"""
|
||||
Select entry points from self that match the
|
||||
given parameters (typically group and/or name).
|
||||
"""
|
||||
return EntryPoints(ep for ep in self if _py39compat.ep_matches(ep, **params))
|
||||
return EntryPoints(ep for ep in self if py39.ep_matches(ep, **params))
|
||||
|
||||
@property
|
||||
def names(self):
|
||||
def names(self) -> Set[str]:
|
||||
"""
|
||||
Return the set of all names of all entry points.
|
||||
"""
|
||||
return {ep.name for ep in self}
|
||||
|
||||
@property
|
||||
def groups(self):
|
||||
def groups(self) -> Set[str]:
|
||||
"""
|
||||
Return the set of all groups of all entry points.
|
||||
"""
|
||||
|
@ -325,47 +311,92 @@ class EntryPoints(tuple):
|
|||
class PackagePath(pathlib.PurePosixPath):
|
||||
"""A reference to a path in a package"""
|
||||
|
||||
def read_text(self, encoding='utf-8'):
|
||||
with self.locate().open(encoding=encoding) as stream:
|
||||
return stream.read()
|
||||
hash: Optional[FileHash]
|
||||
size: int
|
||||
dist: Distribution
|
||||
|
||||
def read_binary(self):
|
||||
with self.locate().open('rb') as stream:
|
||||
return stream.read()
|
||||
def read_text(self, encoding: str = 'utf-8') -> str: # type: ignore[override]
|
||||
return self.locate().read_text(encoding=encoding)
|
||||
|
||||
def locate(self):
|
||||
def read_binary(self) -> bytes:
|
||||
return self.locate().read_bytes()
|
||||
|
||||
def locate(self) -> SimplePath:
|
||||
"""Return a path-like object for this path"""
|
||||
return self.dist.locate_file(self)
|
||||
|
||||
|
||||
class FileHash:
|
||||
def __init__(self, spec):
|
||||
def __init__(self, spec: str) -> None:
|
||||
self.mode, _, self.value = spec.partition('=')
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return f'<FileHash mode: {self.mode} value: {self.value}>'
|
||||
|
||||
|
||||
class Distribution(metaclass=abc.ABCMeta):
|
||||
"""A Python distribution package."""
|
||||
class DeprecatedNonAbstract:
|
||||
# Required until Python 3.14
|
||||
def __new__(cls, *args, **kwargs):
|
||||
all_names = {
|
||||
name for subclass in inspect.getmro(cls) for name in vars(subclass)
|
||||
}
|
||||
abstract = {
|
||||
name
|
||||
for name in all_names
|
||||
if getattr(getattr(cls, name), '__isabstractmethod__', False)
|
||||
}
|
||||
if abstract:
|
||||
warnings.warn(
|
||||
f"Unimplemented abstract methods {abstract}",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return super().__new__(cls)
|
||||
|
||||
|
||||
class Distribution(DeprecatedNonAbstract):
|
||||
"""
|
||||
An abstract Python distribution package.
|
||||
|
||||
Custom providers may derive from this class and define
|
||||
the abstract methods to provide a concrete implementation
|
||||
for their environment. Some providers may opt to override
|
||||
the default implementation of some properties to bypass
|
||||
the file-reading mechanism.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def read_text(self, filename):
|
||||
def read_text(self, filename) -> Optional[str]:
|
||||
"""Attempt to load metadata file given by the name.
|
||||
|
||||
Python distribution metadata is organized by blobs of text
|
||||
typically represented as "files" in the metadata directory
|
||||
(e.g. package-1.0.dist-info). These files include things
|
||||
like:
|
||||
|
||||
- METADATA: The distribution metadata including fields
|
||||
like Name and Version and Description.
|
||||
- entry_points.txt: A series of entry points as defined in
|
||||
`the entry points spec <https://packaging.python.org/en/latest/specifications/entry-points/#file-format>`_.
|
||||
- RECORD: A record of files according to
|
||||
`this recording spec <https://packaging.python.org/en/latest/specifications/recording-installed-packages/#the-record-file>`_.
|
||||
|
||||
A package may provide any set of files, including those
|
||||
not listed here or none at all.
|
||||
|
||||
:param filename: The name of the file in the distribution info.
|
||||
:return: The text if found, otherwise None.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def locate_file(self, path):
|
||||
def locate_file(self, path: str | os.PathLike[str]) -> SimplePath:
|
||||
"""
|
||||
Given a path to a file in this distribution, return a path
|
||||
Given a path to a file in this distribution, return a SimplePath
|
||||
to it.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def from_name(cls, name: str):
|
||||
def from_name(cls, name: str) -> Distribution:
|
||||
"""Return the Distribution for the given package name.
|
||||
|
||||
:param name: The name of the distribution package to search for.
|
||||
|
@ -378,21 +409,23 @@ class Distribution(metaclass=abc.ABCMeta):
|
|||
if not name:
|
||||
raise ValueError("A distribution name is required.")
|
||||
try:
|
||||
return next(cls.discover(name=name))
|
||||
return next(iter(cls.discover(name=name)))
|
||||
except StopIteration:
|
||||
raise PackageNotFoundError(name)
|
||||
|
||||
@classmethod
|
||||
def discover(cls, **kwargs):
|
||||
def discover(
|
||||
cls, *, context: Optional[DistributionFinder.Context] = None, **kwargs
|
||||
) -> Iterable[Distribution]:
|
||||
"""Return an iterable of Distribution objects for all packages.
|
||||
|
||||
Pass a ``context`` or pass keyword arguments for constructing
|
||||
a context.
|
||||
|
||||
:context: A ``DistributionFinder.Context`` object.
|
||||
:return: Iterable of Distribution objects for all packages.
|
||||
:return: Iterable of Distribution objects for packages matching
|
||||
the context.
|
||||
"""
|
||||
context = kwargs.pop('context', None)
|
||||
if context and kwargs:
|
||||
raise ValueError("cannot accept context and kwargs")
|
||||
context = context or DistributionFinder.Context(**kwargs)
|
||||
|
@ -401,8 +434,8 @@ class Distribution(metaclass=abc.ABCMeta):
|
|||
)
|
||||
|
||||
@staticmethod
|
||||
def at(path):
|
||||
"""Return a Distribution for the indicated metadata path
|
||||
def at(path: str | os.PathLike[str]) -> Distribution:
|
||||
"""Return a Distribution for the indicated metadata path.
|
||||
|
||||
:param path: a string or path-like object
|
||||
:return: a concrete Distribution instance for the path
|
||||
|
@ -411,7 +444,7 @@ class Distribution(metaclass=abc.ABCMeta):
|
|||
|
||||
@staticmethod
|
||||
def _discover_resolvers():
|
||||
"""Search the meta_path for resolvers."""
|
||||
"""Search the meta_path for resolvers (MetadataPathFinders)."""
|
||||
declared = (
|
||||
getattr(finder, 'find_distributions', None) for finder in sys.meta_path
|
||||
)
|
||||
|
@ -422,9 +455,13 @@ class Distribution(metaclass=abc.ABCMeta):
|
|||
"""Return the parsed metadata for this Distribution.
|
||||
|
||||
The returned object will have keys that name the various bits of
|
||||
metadata. See PEP 566 for details.
|
||||
metadata per the
|
||||
`Core metadata specifications <https://packaging.python.org/en/latest/specifications/core-metadata/#core-metadata>`_.
|
||||
|
||||
Custom providers may provide the METADATA file or override this
|
||||
property.
|
||||
"""
|
||||
text = (
|
||||
opt_text = (
|
||||
self.read_text('METADATA')
|
||||
or self.read_text('PKG-INFO')
|
||||
# This last clause is here to support old egg-info files. Its
|
||||
|
@ -432,10 +469,11 @@ class Distribution(metaclass=abc.ABCMeta):
|
|||
# (which points to the egg-info file) attribute unchanged.
|
||||
or self.read_text('')
|
||||
)
|
||||
text = cast(str, opt_text)
|
||||
return _adapters.Message(email.message_from_string(text))
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
def name(self) -> str:
|
||||
"""Return the 'Name' metadata for the distribution package."""
|
||||
return self.metadata['Name']
|
||||
|
||||
|
@ -445,24 +483,34 @@ class Distribution(metaclass=abc.ABCMeta):
|
|||
return Prepared.normalize(self.name)
|
||||
|
||||
@property
|
||||
def version(self):
|
||||
def version(self) -> str:
|
||||
"""Return the 'Version' metadata for the distribution package."""
|
||||
return self.metadata['Version']
|
||||
|
||||
@property
|
||||
def entry_points(self):
|
||||
def entry_points(self) -> EntryPoints:
|
||||
"""
|
||||
Return EntryPoints for this distribution.
|
||||
|
||||
Custom providers may provide the ``entry_points.txt`` file
|
||||
or override this property.
|
||||
"""
|
||||
return EntryPoints._from_text_for(self.read_text('entry_points.txt'), self)
|
||||
|
||||
@property
|
||||
def files(self):
|
||||
def files(self) -> Optional[List[PackagePath]]:
|
||||
"""Files in this distribution.
|
||||
|
||||
:return: List of PackagePath for this distribution or None
|
||||
|
||||
Result is `None` if the metadata file that enumerates files
|
||||
(i.e. RECORD for dist-info or SOURCES.txt for egg-info) is
|
||||
missing.
|
||||
(i.e. RECORD for dist-info, or installed-files.txt or
|
||||
SOURCES.txt for egg-info) is missing.
|
||||
Result may be empty if the metadata exists but is empty.
|
||||
|
||||
Custom providers are recommended to provide a "RECORD" file (in
|
||||
``read_text``) or override this property to allow for callers to be
|
||||
able to resolve filenames provided by the package.
|
||||
"""
|
||||
|
||||
def make_file(name, hash=None, size_str=None):
|
||||
|
@ -474,27 +522,76 @@ class Distribution(metaclass=abc.ABCMeta):
|
|||
|
||||
@pass_none
|
||||
def make_files(lines):
|
||||
return list(starmap(make_file, csv.reader(lines)))
|
||||
# Delay csv import, since Distribution.files is not as widely used
|
||||
# as other parts of importlib.metadata
|
||||
import csv
|
||||
|
||||
return make_files(self._read_files_distinfo() or self._read_files_egginfo())
|
||||
return starmap(make_file, csv.reader(lines))
|
||||
|
||||
@pass_none
|
||||
def skip_missing_files(package_paths):
|
||||
return list(filter(lambda path: path.locate().exists(), package_paths))
|
||||
|
||||
return skip_missing_files(
|
||||
make_files(
|
||||
self._read_files_distinfo()
|
||||
or self._read_files_egginfo_installed()
|
||||
or self._read_files_egginfo_sources()
|
||||
)
|
||||
)
|
||||
|
||||
def _read_files_distinfo(self):
|
||||
"""
|
||||
Read the lines of RECORD
|
||||
Read the lines of RECORD.
|
||||
"""
|
||||
text = self.read_text('RECORD')
|
||||
return text and text.splitlines()
|
||||
|
||||
def _read_files_egginfo(self):
|
||||
def _read_files_egginfo_installed(self):
|
||||
"""
|
||||
SOURCES.txt might contain literal commas, so wrap each line
|
||||
in quotes.
|
||||
Read installed-files.txt and return lines in a similar
|
||||
CSV-parsable format as RECORD: each file must be placed
|
||||
relative to the site-packages directory and must also be
|
||||
quoted (since file names can contain literal commas).
|
||||
|
||||
This file is written when the package is installed by pip,
|
||||
but it might not be written for other installation methods.
|
||||
Assume the file is accurate if it exists.
|
||||
"""
|
||||
text = self.read_text('installed-files.txt')
|
||||
# Prepend the .egg-info/ subdir to the lines in this file.
|
||||
# But this subdir is only available from PathDistribution's
|
||||
# self._path.
|
||||
subdir = getattr(self, '_path', None)
|
||||
if not text or not subdir:
|
||||
return
|
||||
|
||||
paths = (
|
||||
(subdir / name)
|
||||
.resolve()
|
||||
.relative_to(self.locate_file('').resolve())
|
||||
.as_posix()
|
||||
for name in text.splitlines()
|
||||
)
|
||||
return map('"{}"'.format, paths)
|
||||
|
||||
def _read_files_egginfo_sources(self):
|
||||
"""
|
||||
Read SOURCES.txt and return lines in a similar CSV-parsable
|
||||
format as RECORD: each file name must be quoted (since it
|
||||
might contain literal commas).
|
||||
|
||||
Note that SOURCES.txt is not a reliable source for what
|
||||
files are installed by a package. This file is generated
|
||||
for a source archive, and the files that are present
|
||||
there (e.g. setup.py) may not correctly reflect the files
|
||||
that are present after the package has been installed.
|
||||
"""
|
||||
text = self.read_text('SOURCES.txt')
|
||||
return text and map('"{}"'.format, text.splitlines())
|
||||
|
||||
@property
|
||||
def requires(self):
|
||||
def requires(self) -> Optional[List[str]]:
|
||||
"""Generated requirements specified for this Distribution"""
|
||||
reqs = self._read_dist_info_reqs() or self._read_egg_info_reqs()
|
||||
return reqs and list(reqs)
|
||||
|
@ -545,10 +642,23 @@ class Distribution(metaclass=abc.ABCMeta):
|
|||
space = url_req_space(section.value)
|
||||
yield section.value + space + quoted_marker(section.name)
|
||||
|
||||
@property
|
||||
def origin(self):
|
||||
return self._load_json('direct_url.json')
|
||||
|
||||
def _load_json(self, filename):
|
||||
return pass_none(json.loads)(
|
||||
self.read_text(filename),
|
||||
object_hook=lambda data: types.SimpleNamespace(**data),
|
||||
)
|
||||
|
||||
|
||||
class DistributionFinder(MetaPathFinder):
|
||||
"""
|
||||
A MetaPathFinder capable of discovering installed distributions.
|
||||
|
||||
Custom providers should implement this interface in order to
|
||||
supply metadata.
|
||||
"""
|
||||
|
||||
class Context:
|
||||
|
@ -561,6 +671,17 @@ class DistributionFinder(MetaPathFinder):
|
|||
Each DistributionFinder may expect any parameters
|
||||
and should attempt to honor the canonical
|
||||
parameters defined below when appropriate.
|
||||
|
||||
This mechanism gives a custom provider a means to
|
||||
solicit additional details from the caller beyond
|
||||
"name" and "path" when searching distributions.
|
||||
For example, imagine a provider that exposes suites
|
||||
of packages in either a "public" or "private" ``realm``.
|
||||
A caller may wish to query only for distributions in
|
||||
a particular realm and could call
|
||||
``distributions(realm="private")`` to signal to the
|
||||
custom provider to only include distributions from that
|
||||
realm.
|
||||
"""
|
||||
|
||||
name = None
|
||||
|
@ -573,7 +694,7 @@ class DistributionFinder(MetaPathFinder):
|
|||
vars(self).update(kwargs)
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
def path(self) -> List[str]:
|
||||
"""
|
||||
The sequence of directory path that a distribution finder
|
||||
should search.
|
||||
|
@ -584,7 +705,7 @@ class DistributionFinder(MetaPathFinder):
|
|||
return vars(self).get('path', sys.path)
|
||||
|
||||
@abc.abstractmethod
|
||||
def find_distributions(self, context=Context()):
|
||||
def find_distributions(self, context=Context()) -> Iterable[Distribution]:
|
||||
"""
|
||||
Find distributions.
|
||||
|
||||
|
@ -596,11 +717,18 @@ class DistributionFinder(MetaPathFinder):
|
|||
|
||||
class FastPath:
|
||||
"""
|
||||
Micro-optimized class for searching a path for
|
||||
children.
|
||||
Micro-optimized class for searching a root for children.
|
||||
|
||||
Root is a path on the file system that may contain metadata
|
||||
directories either as natural directories or within a zip file.
|
||||
|
||||
>>> FastPath('').children()
|
||||
['...']
|
||||
|
||||
FastPath objects are cached and recycled for any given root.
|
||||
|
||||
>>> FastPath('foobar') is FastPath('foobar')
|
||||
True
|
||||
"""
|
||||
|
||||
@functools.lru_cache() # type: ignore
|
||||
|
@ -642,7 +770,19 @@ class FastPath:
|
|||
|
||||
|
||||
class Lookup:
|
||||
"""
|
||||
A micro-optimized class for searching a (fast) path for metadata.
|
||||
"""
|
||||
|
||||
def __init__(self, path: FastPath):
|
||||
"""
|
||||
Calculate all of the children representing metadata.
|
||||
|
||||
From the children in the path, calculate early all of the
|
||||
children that appear to represent metadata (infos) or legacy
|
||||
metadata (eggs).
|
||||
"""
|
||||
|
||||
base = os.path.basename(path.root).lower()
|
||||
base_is_egg = base.endswith(".egg")
|
||||
self.infos = FreezableDefaultDict(list)
|
||||
|
@ -663,7 +803,10 @@ class Lookup:
|
|||
self.infos.freeze()
|
||||
self.eggs.freeze()
|
||||
|
||||
def search(self, prepared):
|
||||
def search(self, prepared: Prepared):
|
||||
"""
|
||||
Yield all infos and eggs matching the Prepared query.
|
||||
"""
|
||||
infos = (
|
||||
self.infos[prepared.normalized]
|
||||
if prepared
|
||||
|
@ -679,13 +822,28 @@ class Lookup:
|
|||
|
||||
class Prepared:
|
||||
"""
|
||||
A prepared search for metadata on a possibly-named package.
|
||||
A prepared search query for metadata on a possibly-named package.
|
||||
|
||||
Pre-calculates the normalization to prevent repeated operations.
|
||||
|
||||
>>> none = Prepared(None)
|
||||
>>> none.normalized
|
||||
>>> none.legacy_normalized
|
||||
>>> bool(none)
|
||||
False
|
||||
>>> sample = Prepared('Sample__Pkg-name.foo')
|
||||
>>> sample.normalized
|
||||
'sample_pkg_name_foo'
|
||||
>>> sample.legacy_normalized
|
||||
'sample__pkg_name.foo'
|
||||
>>> bool(sample)
|
||||
True
|
||||
"""
|
||||
|
||||
normalized = None
|
||||
legacy_normalized = None
|
||||
|
||||
def __init__(self, name):
|
||||
def __init__(self, name: Optional[str]):
|
||||
self.name = name
|
||||
if name is None:
|
||||
return
|
||||
|
@ -719,7 +877,10 @@ class MetadataPathFinder(NullFinder, DistributionFinder):
|
|||
of Python that do not have a PathFinder find_distributions().
|
||||
"""
|
||||
|
||||
def find_distributions(self, context=DistributionFinder.Context()):
|
||||
@classmethod
|
||||
def find_distributions(
|
||||
cls, context=DistributionFinder.Context()
|
||||
) -> Iterable[PathDistribution]:
|
||||
"""
|
||||
Find distributions.
|
||||
|
||||
|
@ -728,7 +889,7 @@ class MetadataPathFinder(NullFinder, DistributionFinder):
|
|||
(or all names if ``None`` indicated) along the paths in the list
|
||||
of directories ``context.path``.
|
||||
"""
|
||||
found = self._search_paths(context.name, context.path)
|
||||
found = cls._search_paths(context.name, context.path)
|
||||
return map(PathDistribution, found)
|
||||
|
||||
@classmethod
|
||||
|
@ -739,19 +900,20 @@ class MetadataPathFinder(NullFinder, DistributionFinder):
|
|||
path.search(prepared) for path in map(FastPath, paths)
|
||||
)
|
||||
|
||||
def invalidate_caches(cls):
|
||||
@classmethod
|
||||
def invalidate_caches(cls) -> None:
|
||||
FastPath.__new__.cache_clear()
|
||||
|
||||
|
||||
class PathDistribution(Distribution):
|
||||
def __init__(self, path: SimplePath):
|
||||
def __init__(self, path: SimplePath) -> None:
|
||||
"""Construct a distribution.
|
||||
|
||||
:param path: SimplePath indicating the metadata directory.
|
||||
"""
|
||||
self._path = path
|
||||
|
||||
def read_text(self, filename):
|
||||
def read_text(self, filename: str | os.PathLike[str]) -> Optional[str]:
|
||||
with suppress(
|
||||
FileNotFoundError,
|
||||
IsADirectoryError,
|
||||
|
@ -761,9 +923,11 @@ class PathDistribution(Distribution):
|
|||
):
|
||||
return self._path.joinpath(filename).read_text(encoding='utf-8')
|
||||
|
||||
return None
|
||||
|
||||
read_text.__doc__ = Distribution.read_text.__doc__
|
||||
|
||||
def locate_file(self, path):
|
||||
def locate_file(self, path: str | os.PathLike[str]) -> SimplePath:
|
||||
return self._path.parent / path
|
||||
|
||||
@property
|
||||
|
@ -796,7 +960,7 @@ class PathDistribution(Distribution):
|
|||
return name
|
||||
|
||||
|
||||
def distribution(distribution_name):
|
||||
def distribution(distribution_name: str) -> Distribution:
|
||||
"""Get the ``Distribution`` instance for the named package.
|
||||
|
||||
:param distribution_name: The name of the distribution package as a string.
|
||||
|
@ -805,7 +969,7 @@ def distribution(distribution_name):
|
|||
return Distribution.from_name(distribution_name)
|
||||
|
||||
|
||||
def distributions(**kwargs):
|
||||
def distributions(**kwargs) -> Iterable[Distribution]:
|
||||
"""Get all ``Distribution`` instances in the current environment.
|
||||
|
||||
:return: An iterable of ``Distribution`` instances.
|
||||
|
@ -813,7 +977,7 @@ def distributions(**kwargs):
|
|||
return Distribution.discover(**kwargs)
|
||||
|
||||
|
||||
def metadata(distribution_name) -> _meta.PackageMetadata:
|
||||
def metadata(distribution_name: str) -> _meta.PackageMetadata:
|
||||
"""Get the metadata for the named package.
|
||||
|
||||
:param distribution_name: The name of the distribution package to query.
|
||||
|
@ -822,7 +986,7 @@ def metadata(distribution_name) -> _meta.PackageMetadata:
|
|||
return Distribution.from_name(distribution_name).metadata
|
||||
|
||||
|
||||
def version(distribution_name):
|
||||
def version(distribution_name: str) -> str:
|
||||
"""Get the version string for the named package.
|
||||
|
||||
:param distribution_name: The name of the distribution package to query.
|
||||
|
@ -834,7 +998,7 @@ def version(distribution_name):
|
|||
|
||||
_unique = functools.partial(
|
||||
unique_everseen,
|
||||
key=_py39compat.normalized_name,
|
||||
key=py39.normalized_name,
|
||||
)
|
||||
"""
|
||||
Wrapper for ``distributions`` to return unique distributions by name.
|
||||
|
@ -856,7 +1020,7 @@ def entry_points(**params) -> EntryPoints:
|
|||
return EntryPoints(eps).select(**params)
|
||||
|
||||
|
||||
def files(distribution_name):
|
||||
def files(distribution_name: str) -> Optional[List[PackagePath]]:
|
||||
"""Return a list of files for the named package.
|
||||
|
||||
:param distribution_name: The name of the distribution package to query.
|
||||
|
@ -865,11 +1029,11 @@ def files(distribution_name):
|
|||
return distribution(distribution_name).files
|
||||
|
||||
|
||||
def requires(distribution_name):
|
||||
def requires(distribution_name: str) -> Optional[List[str]]:
|
||||
"""
|
||||
Return a list of requirements for the named package.
|
||||
|
||||
:return: An iterator of requirements, suitable for
|
||||
:return: An iterable of requirements, suitable for
|
||||
packaging.requirement.Requirement.
|
||||
"""
|
||||
return distribution(distribution_name).requires
|
||||
|
@ -896,9 +1060,43 @@ def _top_level_declared(dist):
|
|||
return (dist.read_text('top_level.txt') or '').split()
|
||||
|
||||
|
||||
def _topmost(name: PackagePath) -> Optional[str]:
|
||||
"""
|
||||
Return the top-most parent as long as there is a parent.
|
||||
"""
|
||||
top, *rest = name.parts
|
||||
return top if rest else None
|
||||
|
||||
|
||||
def _get_toplevel_name(name: PackagePath) -> str:
|
||||
"""
|
||||
Infer a possibly importable module name from a name presumed on
|
||||
sys.path.
|
||||
|
||||
>>> _get_toplevel_name(PackagePath('foo.py'))
|
||||
'foo'
|
||||
>>> _get_toplevel_name(PackagePath('foo'))
|
||||
'foo'
|
||||
>>> _get_toplevel_name(PackagePath('foo.pyc'))
|
||||
'foo'
|
||||
>>> _get_toplevel_name(PackagePath('foo/__init__.py'))
|
||||
'foo'
|
||||
>>> _get_toplevel_name(PackagePath('foo.pth'))
|
||||
'foo.pth'
|
||||
>>> _get_toplevel_name(PackagePath('foo.dist-info'))
|
||||
'foo.dist-info'
|
||||
"""
|
||||
return _topmost(name) or (
|
||||
# python/typeshed#10328
|
||||
inspect.getmodulename(name) # type: ignore
|
||||
or str(name)
|
||||
)
|
||||
|
||||
|
||||
def _top_level_inferred(dist):
|
||||
return {
|
||||
f.parts[0] if len(f.parts) > 1 else f.with_suffix('').name
|
||||
for f in always_iterable(dist.files)
|
||||
if f.suffix == ".py"
|
||||
}
|
||||
opt_names = set(map(_get_toplevel_name, always_iterable(dist.files)))
|
||||
|
||||
def importable_name(name):
|
||||
return '.' not in name
|
||||
|
||||
return filter(importable_name, opt_names)
|
||||
|
|
|
@ -54,7 +54,7 @@ class Message(email.message.Message):
|
|||
def __getitem__(self, item):
|
||||
"""
|
||||
Warn users that a ``KeyError`` can be expected when a
|
||||
mising key is supplied. Ref python/importlib_metadata#371.
|
||||
missing key is supplied. Ref python/importlib_metadata#371.
|
||||
"""
|
||||
res = super().__getitem__(item)
|
||||
if res is None:
|
||||
|
|
|
@ -2,14 +2,7 @@ import sys
|
|||
import platform
|
||||
|
||||
|
||||
__all__ = ['install', 'NullFinder', 'Protocol']
|
||||
|
||||
|
||||
try:
|
||||
from typing import Protocol
|
||||
except ImportError: # pragma: no cover
|
||||
# Python 3.7 compatibility
|
||||
from typing_extensions import Protocol # type: ignore
|
||||
__all__ = ['install', 'NullFinder']
|
||||
|
||||
|
||||
def install(cls):
|
||||
|
@ -45,7 +38,7 @@ def disable_stdlib_finder():
|
|||
|
||||
class NullFinder:
|
||||
"""
|
||||
A "Finder" (aka "MetaClassFinder") that never finds any modules,
|
||||
A "Finder" (aka "MetaPathFinder") that never finds any modules,
|
||||
but may find distributions.
|
||||
"""
|
||||
|
||||
|
@ -53,14 +46,6 @@ class NullFinder:
|
|||
def find_spec(*args, **kwargs):
|
||||
return None
|
||||
|
||||
# In Python 2, the import system requires finders
|
||||
# to have a find_module() method, but this usage
|
||||
# is deprecated in Python 3 in favor of find_spec().
|
||||
# For the purposes of this finder (i.e. being present
|
||||
# on sys.meta_path but having no other import
|
||||
# system functionality), the two methods are identical.
|
||||
find_module = find_spec
|
||||
|
||||
|
||||
def pypy_partial(val):
|
||||
"""
|
||||
|
|
|
@ -1,24 +1,38 @@
|
|||
from ._compat import Protocol
|
||||
from typing import Any, Dict, Iterator, List, TypeVar, Union
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Protocol
|
||||
from typing import Any, Dict, Iterator, List, Optional, TypeVar, Union, overload
|
||||
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
|
||||
class PackageMetadata(Protocol):
|
||||
def __len__(self) -> int:
|
||||
... # pragma: no cover
|
||||
def __len__(self) -> int: ... # pragma: no cover
|
||||
|
||||
def __contains__(self, item: str) -> bool:
|
||||
... # pragma: no cover
|
||||
def __contains__(self, item: str) -> bool: ... # pragma: no cover
|
||||
|
||||
def __getitem__(self, key: str) -> str:
|
||||
... # pragma: no cover
|
||||
def __getitem__(self, key: str) -> str: ... # pragma: no cover
|
||||
|
||||
def __iter__(self) -> Iterator[str]:
|
||||
... # pragma: no cover
|
||||
def __iter__(self) -> Iterator[str]: ... # pragma: no cover
|
||||
|
||||
def get_all(self, name: str, failobj: _T = ...) -> Union[List[Any], _T]:
|
||||
@overload
|
||||
def get(
|
||||
self, name: str, failobj: None = None
|
||||
) -> Optional[str]: ... # pragma: no cover
|
||||
|
||||
@overload
|
||||
def get(self, name: str, failobj: _T) -> Union[str, _T]: ... # pragma: no cover
|
||||
|
||||
# overload per python/importlib_metadata#435
|
||||
@overload
|
||||
def get_all(
|
||||
self, name: str, failobj: None = None
|
||||
) -> Optional[List[Any]]: ... # pragma: no cover
|
||||
|
||||
@overload
|
||||
def get_all(self, name: str, failobj: _T) -> Union[List[Any], _T]:
|
||||
"""
|
||||
Return all values associated with a possibly multi-valued key.
|
||||
"""
|
||||
|
@ -30,20 +44,24 @@ class PackageMetadata(Protocol):
|
|||
"""
|
||||
|
||||
|
||||
class SimplePath(Protocol[_T]):
|
||||
class SimplePath(Protocol):
|
||||
"""
|
||||
A minimal subset of pathlib.Path required by PathDistribution.
|
||||
A minimal subset of pathlib.Path required by Distribution.
|
||||
"""
|
||||
|
||||
def joinpath(self) -> _T:
|
||||
... # pragma: no cover
|
||||
def joinpath(
|
||||
self, other: Union[str, os.PathLike[str]]
|
||||
) -> SimplePath: ... # pragma: no cover
|
||||
|
||||
def __truediv__(self, other: Union[str, _T]) -> _T:
|
||||
... # pragma: no cover
|
||||
def __truediv__(
|
||||
self, other: Union[str, os.PathLike[str]]
|
||||
) -> SimplePath: ... # pragma: no cover
|
||||
|
||||
@property
|
||||
def parent(self) -> _T:
|
||||
... # pragma: no cover
|
||||
def parent(self) -> SimplePath: ... # pragma: no cover
|
||||
|
||||
def read_text(self) -> str:
|
||||
... # pragma: no cover
|
||||
def read_text(self, encoding=None) -> str: ... # pragma: no cover
|
||||
|
||||
def read_bytes(self) -> bytes: ... # pragma: no cover
|
||||
|
||||
def exists(self) -> bool: ... # pragma: no cover
|
||||
|
|
0
lib/importlib_metadata/compat/__init__.py
Normal file
0
lib/importlib_metadata/compat/__init__.py
Normal file
|
@ -1,11 +1,12 @@
|
|||
"""
|
||||
Compatibility layer with Python 3.8/3.9
|
||||
"""
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Optional
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
# Prevent circular imports on runtime.
|
||||
from . import Distribution, EntryPoint
|
||||
from .. import Distribution, EntryPoint
|
||||
else:
|
||||
Distribution = EntryPoint = Any
|
||||
|
||||
|
@ -17,7 +18,7 @@ def normalized_name(dist: Distribution) -> Optional[str]:
|
|||
try:
|
||||
return dist._normalized_name
|
||||
except AttributeError:
|
||||
from . import Prepared # -> delay to prevent circular imports.
|
||||
from .. import Prepared # -> delay to prevent circular imports.
|
||||
|
||||
return Prepared.normalize(getattr(dist, "name", None) or dist.metadata['Name'])
|
||||
|
||||
|
@ -29,7 +30,7 @@ def ep_matches(ep: EntryPoint, **params) -> bool:
|
|||
try:
|
||||
return ep.matches(**params)
|
||||
except AttributeError:
|
||||
from . import EntryPoint # -> delay to prevent circular imports.
|
||||
from .. import EntryPoint # -> delay to prevent circular imports.
|
||||
|
||||
# Reconstruct the EntryPoint object to make sure it is compatible.
|
||||
return EntryPoint(ep.name, ep.value, ep.group).matches(**params)
|
21
lib/importlib_metadata/diagnose.py
Normal file
21
lib/importlib_metadata/diagnose.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
import sys
|
||||
|
||||
from . import Distribution
|
||||
|
||||
|
||||
def inspect(path):
|
||||
print("Inspecting", path)
|
||||
dists = list(Distribution.discover(path=[path]))
|
||||
if not dists:
|
||||
return
|
||||
print("Found", len(dists), "packages:", end=' ')
|
||||
print(', '.join(dist.name for dist in dists))
|
||||
|
||||
|
||||
def run():
|
||||
for path in sys.path:
|
||||
inspect(path)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
run()
|
|
@ -5,9 +5,10 @@ import itertools
|
|||
import contextlib
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
|
||||
from .py310compat import text_encoding
|
||||
from .glob import translate
|
||||
from .compat.py310 import text_encoding
|
||||
from .glob import Translator
|
||||
|
||||
|
||||
__all__ = ['Path']
|
||||
|
@ -148,6 +149,16 @@ class CompleteDirs(InitializedState, zipfile.ZipFile):
|
|||
source.__class__ = cls
|
||||
return source
|
||||
|
||||
@classmethod
|
||||
def inject(cls, zf: zipfile.ZipFile) -> zipfile.ZipFile:
|
||||
"""
|
||||
Given a writable zip file zf, inject directory entries for
|
||||
any directories implied by the presence of children.
|
||||
"""
|
||||
for name in cls._implied_dirs(zf.namelist()):
|
||||
zf.writestr(name, b"")
|
||||
return zf
|
||||
|
||||
|
||||
class FastLookup(CompleteDirs):
|
||||
"""
|
||||
|
@ -169,8 +180,10 @@ class FastLookup(CompleteDirs):
|
|||
|
||||
|
||||
def _extract_text_encoding(encoding=None, *args, **kwargs):
|
||||
# stacklevel=3 so that the caller of the caller see any warning.
|
||||
return text_encoding(encoding, 3), args, kwargs
|
||||
# compute stack level so that the caller of the caller sees any warning.
|
||||
is_pypy = sys.implementation.name == 'pypy'
|
||||
stack_level = 3 + is_pypy
|
||||
return text_encoding(encoding, stack_level), args, kwargs
|
||||
|
||||
|
||||
class Path:
|
||||
|
@ -195,13 +208,13 @@ class Path:
|
|||
|
||||
Path accepts the zipfile object itself or a filename
|
||||
|
||||
>>> root = Path(zf)
|
||||
>>> path = Path(zf)
|
||||
|
||||
From there, several path operations are available.
|
||||
|
||||
Directory iteration (including the zip file itself):
|
||||
|
||||
>>> a, b = root.iterdir()
|
||||
>>> a, b = path.iterdir()
|
||||
>>> a
|
||||
Path('mem/abcde.zip', 'a.txt')
|
||||
>>> b
|
||||
|
@ -239,16 +252,38 @@ class Path:
|
|||
'mem/abcde.zip/b/c.txt'
|
||||
|
||||
At the root, ``name``, ``filename``, and ``parent``
|
||||
resolve to the zipfile. Note these attributes are not
|
||||
valid and will raise a ``ValueError`` if the zipfile
|
||||
has no filename.
|
||||
resolve to the zipfile.
|
||||
|
||||
>>> root.name
|
||||
>>> str(path)
|
||||
'mem/abcde.zip/'
|
||||
>>> path.name
|
||||
'abcde.zip'
|
||||
>>> str(root.filename).replace(os.sep, posixpath.sep)
|
||||
'mem/abcde.zip'
|
||||
>>> str(root.parent)
|
||||
>>> path.filename == pathlib.Path('mem/abcde.zip')
|
||||
True
|
||||
>>> str(path.parent)
|
||||
'mem'
|
||||
|
||||
If the zipfile has no filename, such attribtues are not
|
||||
valid and accessing them will raise an Exception.
|
||||
|
||||
>>> zf.filename = None
|
||||
>>> path.name
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
TypeError: ...
|
||||
|
||||
>>> path.filename
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
TypeError: ...
|
||||
|
||||
>>> path.parent
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
TypeError: ...
|
||||
|
||||
# workaround python/cpython#106763
|
||||
>>> pass
|
||||
"""
|
||||
|
||||
__repr = "{self.__class__.__name__}({self.root.filename!r}, {self.at!r})"
|
||||
|
@ -365,8 +400,10 @@ class Path:
|
|||
raise ValueError(f"Unacceptable pattern: {pattern!r}")
|
||||
|
||||
prefix = re.escape(self.at)
|
||||
matches = re.compile(prefix + translate(pattern)).fullmatch
|
||||
return map(self._next, filter(matches, self.root.namelist()))
|
||||
tr = Translator(seps='/')
|
||||
matches = re.compile(prefix + tr.translate(pattern)).fullmatch
|
||||
names = (data.filename for data in self.root.filelist)
|
||||
return map(self._next, filter(matches, names))
|
||||
|
||||
def rglob(self, pattern):
|
||||
return self.glob(f'**/{pattern}')
|
||||
|
|
0
lib/zipp/compat/__init__.py
Normal file
0
lib/zipp/compat/__init__.py
Normal file
106
lib/zipp/glob.py
106
lib/zipp/glob.py
|
@ -1,18 +1,97 @@
|
|||
import os
|
||||
import re
|
||||
|
||||
|
||||
def translate(pattern):
|
||||
_default_seps = os.sep + str(os.altsep) * bool(os.altsep)
|
||||
|
||||
|
||||
class Translator:
|
||||
"""
|
||||
>>> Translator('xyz')
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
AssertionError: Invalid separators
|
||||
|
||||
>>> Translator('')
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
AssertionError: Invalid separators
|
||||
"""
|
||||
|
||||
seps: str
|
||||
|
||||
def __init__(self, seps: str = _default_seps):
|
||||
assert seps and set(seps) <= set(_default_seps), "Invalid separators"
|
||||
self.seps = seps
|
||||
|
||||
def translate(self, pattern):
|
||||
"""
|
||||
Given a glob pattern, produce a regex that matches it.
|
||||
"""
|
||||
return self.extend(self.translate_core(pattern))
|
||||
|
||||
def extend(self, pattern):
|
||||
r"""
|
||||
Extend regex for pattern-wide concerns.
|
||||
|
||||
Apply '(?s:)' to create a non-matching group that
|
||||
matches newlines (valid on Unix).
|
||||
|
||||
Append '\Z' to imply fullmatch even when match is used.
|
||||
"""
|
||||
return rf'(?s:{pattern})\Z'
|
||||
|
||||
def translate_core(self, pattern):
|
||||
r"""
|
||||
Given a glob pattern, produce a regex that matches it.
|
||||
|
||||
>>> translate('*.txt')
|
||||
>>> t = Translator()
|
||||
>>> t.translate_core('*.txt').replace('\\\\', '')
|
||||
'[^/]*\\.txt'
|
||||
>>> translate('a?txt')
|
||||
'a.txt'
|
||||
>>> translate('**/*')
|
||||
'.*/[^/]*'
|
||||
>>> t.translate_core('a?txt')
|
||||
'a[^/]txt'
|
||||
>>> t.translate_core('**/*').replace('\\\\', '')
|
||||
'.*/[^/][^/]*'
|
||||
"""
|
||||
return ''.join(map(replace, separate(pattern)))
|
||||
self.restrict_rglob(pattern)
|
||||
return ''.join(map(self.replace, separate(self.star_not_empty(pattern))))
|
||||
|
||||
def replace(self, match):
|
||||
"""
|
||||
Perform the replacements for a match from :func:`separate`.
|
||||
"""
|
||||
return match.group('set') or (
|
||||
re.escape(match.group(0))
|
||||
.replace('\\*\\*', r'.*')
|
||||
.replace('\\*', rf'[^{re.escape(self.seps)}]*')
|
||||
.replace('\\?', r'[^/]')
|
||||
)
|
||||
|
||||
def restrict_rglob(self, pattern):
|
||||
"""
|
||||
Raise ValueError if ** appears in anything but a full path segment.
|
||||
|
||||
>>> Translator().translate('**foo')
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: ** must appear alone in a path segment
|
||||
"""
|
||||
seps_pattern = rf'[{re.escape(self.seps)}]+'
|
||||
segments = re.split(seps_pattern, pattern)
|
||||
if any('**' in segment and segment != '**' for segment in segments):
|
||||
raise ValueError("** must appear alone in a path segment")
|
||||
|
||||
def star_not_empty(self, pattern):
|
||||
"""
|
||||
Ensure that * will not match an empty segment.
|
||||
"""
|
||||
|
||||
def handle_segment(match):
|
||||
segment = match.group(0)
|
||||
return '?*' if segment == '*' else segment
|
||||
|
||||
not_seps_pattern = rf'[^{re.escape(self.seps)}]+'
|
||||
return re.sub(not_seps_pattern, handle_segment, pattern)
|
||||
|
||||
|
||||
def separate(pattern):
|
||||
|
@ -25,16 +104,3 @@ def separate(pattern):
|
|||
['a', '[?]', 'txt']
|
||||
"""
|
||||
return re.finditer(r'([^\[]+)|(?P<set>[\[].*?[\]])|([\[][^\]]*$)', pattern)
|
||||
|
||||
|
||||
def replace(match):
|
||||
"""
|
||||
Perform the replacements for a match from :func:`separate`.
|
||||
"""
|
||||
|
||||
return match.group('set') or (
|
||||
re.escape(match.group(0))
|
||||
.replace('\\*\\*', r'.*')
|
||||
.replace('\\*', r'[^/]*')
|
||||
.replace('\\?', r'.')
|
||||
)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue