fpathlib 0.1.3__tar.gz → 0.1.4__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.
- {fpathlib-0.1.3 → fpathlib-0.1.4}/.gitignore +1 -0
- {fpathlib-0.1.3/src/fpathlib.egg-info → fpathlib-0.1.4}/PKG-INFO +1 -1
- fpathlib-0.1.4/conda-recipe/meta.yaml +39 -0
- fpathlib-0.1.4/scripts/conda.sh +50 -0
- fpathlib-0.1.4/scripts/deploy.sh +13 -0
- fpathlib-0.1.4/scripts/pypi.sh +35 -0
- fpathlib-0.1.4/scripts/tag.sh +56 -0
- {fpathlib-0.1.3 → fpathlib-0.1.4}/src/fpathlib/__init__.py +2 -0
- {fpathlib-0.1.3 → fpathlib-0.1.4}/src/fpathlib/_version.py +3 -3
- {fpathlib-0.1.3 → fpathlib-0.1.4}/src/fpathlib/expand.py +34 -1
- {fpathlib-0.1.3 → fpathlib-0.1.4}/src/fpathlib/ext/polars.py +32 -0
- {fpathlib-0.1.3 → fpathlib-0.1.4}/src/fpathlib/fpath.py +86 -15
- {fpathlib-0.1.3 → fpathlib-0.1.4/src/fpathlib.egg-info}/PKG-INFO +1 -1
- {fpathlib-0.1.3 → fpathlib-0.1.4}/src/fpathlib.egg-info/SOURCES.txt +2 -0
- {fpathlib-0.1.3 → fpathlib-0.1.4}/src/fpathlib.egg-info/scm_file_list.json +2 -0
- fpathlib-0.1.4/src/fpathlib.egg-info/scm_version.json +8 -0
- fpathlib-0.1.3/scripts/deploy.sh +0 -3
- fpathlib-0.1.3/scripts/pypi.sh +0 -20
- fpathlib-0.1.3/scripts/tag.sh +0 -33
- fpathlib-0.1.3/src/fpathlib.egg-info/scm_version.json +0 -8
- {fpathlib-0.1.3 → fpathlib-0.1.4}/LICENSE +0 -0
- {fpathlib-0.1.3 → fpathlib-0.1.4}/MANIFEST.in +0 -0
- {fpathlib-0.1.3 → fpathlib-0.1.4}/README.md +0 -0
- {fpathlib-0.1.3 → fpathlib-0.1.4}/TODO.txt +0 -0
- {fpathlib-0.1.3 → fpathlib-0.1.4}/docs/Makefile +0 -0
- {fpathlib-0.1.3 → fpathlib-0.1.4}/docs/make.bat +0 -0
- {fpathlib-0.1.3 → fpathlib-0.1.4}/docs/source/api.rst +0 -0
- {fpathlib-0.1.3 → fpathlib-0.1.4}/docs/source/conf.py +0 -0
- {fpathlib-0.1.3 → fpathlib-0.1.4}/docs/source/index.rst +0 -0
- {fpathlib-0.1.3 → fpathlib-0.1.4}/pyproject.toml +0 -0
- {fpathlib-0.1.3 → fpathlib-0.1.4}/scripts/docs.sh +0 -0
- {fpathlib-0.1.3 → fpathlib-0.1.4}/setup.cfg +0 -0
- {fpathlib-0.1.3 → fpathlib-0.1.4}/src/fpathlib/ext/__init__.py +0 -0
- {fpathlib-0.1.3 → fpathlib-0.1.4}/src/fpathlib/path.py +0 -0
- {fpathlib-0.1.3 → fpathlib-0.1.4}/src/fpathlib/utils.py +0 -0
- {fpathlib-0.1.3 → fpathlib-0.1.4}/src/fpathlib.egg-info/dependency_links.txt +0 -0
- {fpathlib-0.1.3 → fpathlib-0.1.4}/src/fpathlib.egg-info/requires.txt +0 -0
- {fpathlib-0.1.3 → fpathlib-0.1.4}/src/fpathlib.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{% set name = "fpathlib" %}
|
|
2
|
+
{% set version = "0.1.3" %}
|
|
3
|
+
|
|
4
|
+
package:
|
|
5
|
+
name: {{ name|lower }}
|
|
6
|
+
version: {{ version }}
|
|
7
|
+
|
|
8
|
+
source:
|
|
9
|
+
url: https://pypi.io/packages/source/{{ name[0] }}/{{ name }}/fpathlib-{{ version }}.tar.gz
|
|
10
|
+
sha256: 059dbf937c6e965ef33f15aa214b2f354550d5dfa26b9030357954cce8c51874
|
|
11
|
+
|
|
12
|
+
build:
|
|
13
|
+
noarch: python
|
|
14
|
+
number: 0
|
|
15
|
+
script: {{ PYTHON }} -m pip install . -vv --no-deps --no-build-isolation
|
|
16
|
+
|
|
17
|
+
requirements:
|
|
18
|
+
host:
|
|
19
|
+
- python >=3.12
|
|
20
|
+
- pip
|
|
21
|
+
- setuptools >=68
|
|
22
|
+
- setuptools_scm
|
|
23
|
+
- wheel
|
|
24
|
+
run:
|
|
25
|
+
- python >=3.12
|
|
26
|
+
- parse
|
|
27
|
+
|
|
28
|
+
test:
|
|
29
|
+
imports:
|
|
30
|
+
- fpathlib
|
|
31
|
+
|
|
32
|
+
about:
|
|
33
|
+
summary: A package to combine paths with metadata
|
|
34
|
+
license: MIT
|
|
35
|
+
home: https://github.com/LockhartLab/fpathlib
|
|
36
|
+
|
|
37
|
+
extra:
|
|
38
|
+
recipe-maintainers:
|
|
39
|
+
- clockhart
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Builds and uploads the conda package for the version pypi.sh just
|
|
2
|
+
# published, to the lockhartlab org on anaconda.org. Must run AFTER
|
|
3
|
+
# pypi.sh, since it reads the sdist checksum back from PyPI's JSON API
|
|
4
|
+
# (retrying briefly -- new uploads take a few seconds to index).
|
|
5
|
+
#
|
|
6
|
+
# Requires a local micromamba env named "fpathlib-build" with conda-build
|
|
7
|
+
# and anaconda-client installed, and an active `anaconda login --at
|
|
8
|
+
# anaconda.org` session for an account with upload rights to lockhartlab:
|
|
9
|
+
# micromamba create -n fpathlib-build -c conda-forge conda-build anaconda-client
|
|
10
|
+
# micromamba run -n fpathlib-build anaconda login --at anaconda.org
|
|
11
|
+
|
|
12
|
+
version=$(git describe --tags --abbrev=0)
|
|
13
|
+
|
|
14
|
+
sha256=""
|
|
15
|
+
for i in 1 2 3 4 5
|
|
16
|
+
do
|
|
17
|
+
sha256=$(curl -s "https://pypi.org/pypi/fpathlib/$version/json" | python3 -c "
|
|
18
|
+
import json, sys
|
|
19
|
+
try:
|
|
20
|
+
d = json.load(sys.stdin)
|
|
21
|
+
for f in d['urls']:
|
|
22
|
+
if f['filename'].endswith('.tar.gz'):
|
|
23
|
+
print(f['digests']['sha256'])
|
|
24
|
+
break
|
|
25
|
+
except Exception:
|
|
26
|
+
pass
|
|
27
|
+
")
|
|
28
|
+
if [ -n "$sha256" ]
|
|
29
|
+
then
|
|
30
|
+
break
|
|
31
|
+
fi
|
|
32
|
+
sleep 5
|
|
33
|
+
done
|
|
34
|
+
|
|
35
|
+
if [ -z "$sha256" ]
|
|
36
|
+
then
|
|
37
|
+
echo "could not fetch sdist sha256 from PyPI for $version yet, skipping conda build -- rerun scripts/conda.sh once PyPI has indexed the release"
|
|
38
|
+
return
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
sed -i '' -E "s/\{% set version = \"[^\"]+\" %\}/{% set version = \"$version\" %}/" conda-recipe/meta.yaml
|
|
42
|
+
sed -i '' -E "s/sha256: .+/sha256: $sha256/" conda-recipe/meta.yaml
|
|
43
|
+
|
|
44
|
+
rm -rf /tmp/conda-build-out
|
|
45
|
+
micromamba run -n fpathlib-build conda-build conda-recipe -c conda-forge --output-folder /tmp/conda-build-out
|
|
46
|
+
micromamba run -n fpathlib-build anaconda upload "/tmp/conda-build-out/noarch/fpathlib-$version-py_0.conda" --user lockhartlab
|
|
47
|
+
|
|
48
|
+
git add conda-recipe/meta.yaml
|
|
49
|
+
git commit -m "conda-recipe: bump to $version"
|
|
50
|
+
git push origin main
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Full release: build docs, bump+push the patch tag, build and upload to
|
|
2
|
+
# PyPI, then build and upload to conda (lockhartlab org). Order matters --
|
|
3
|
+
# tag.sh must run before pypi.sh so that HEAD is exactly on the new tag
|
|
4
|
+
# (clean release, no .devN) when pypi.sh builds, and conda.sh must run
|
|
5
|
+
# after pypi.sh since it packages what was just published to PyPI.
|
|
6
|
+
# For an interim dev release without cutting a tag, run
|
|
7
|
+
# `source scripts/pypi.sh dev` directly instead of this script.
|
|
8
|
+
set -e
|
|
9
|
+
|
|
10
|
+
source scripts/docs.sh
|
|
11
|
+
source scripts/tag.sh "patch"
|
|
12
|
+
source scripts/pypi.sh
|
|
13
|
+
source scripts/conda.sh
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Builds and (usually) uploads to PyPI. setuptools_scm derives the version
|
|
2
|
+
# purely from git: exactly on a tag with a clean tree -> "X.Y.Z"; ahead of
|
|
3
|
+
# the last tag, or a dirty tree -> "X.Y.Z.devN". Run this on its own (e.g.
|
|
4
|
+
# `source scripts/pypi.sh dev`) to intentionally publish an interim dev
|
|
5
|
+
# build while iterating; deploy.sh runs tag.sh first so that by the time
|
|
6
|
+
# this script runs, HEAD should be exactly on the new tag and produce a
|
|
7
|
+
# clean release -- if it doesn't, something upstream is wrong (see below).
|
|
8
|
+
allow_dev=0
|
|
9
|
+
if [ "$1" = "dev" ]
|
|
10
|
+
then
|
|
11
|
+
allow_dev=1
|
|
12
|
+
fi
|
|
13
|
+
|
|
14
|
+
# Stale dist/ or egg-info from a previous failed run can leak into `twine
|
|
15
|
+
# upload dist/*` (multiple versions at once) or shadow a fresh build.
|
|
16
|
+
rm -rf dist src/fpathlib.egg-info
|
|
17
|
+
|
|
18
|
+
python3 -m build
|
|
19
|
+
|
|
20
|
+
# Belt-and-suspenders check: rather than trust git state ahead of time (the
|
|
21
|
+
# 0.1.3/0.1.2 tag-collision incident slipped past a git-describe check even
|
|
22
|
+
# though the build itself came out as a .devN), inspect what actually got
|
|
23
|
+
# built. This is the real signal of whether the release is clean.
|
|
24
|
+
dev_artifacts=$(ls dist/ | grep -c '\.dev[0-9]')
|
|
25
|
+
if [ $allow_dev -eq 0 ] && [ "$dev_artifacts" != "0" ]
|
|
26
|
+
then
|
|
27
|
+
echo "built version is a dev version (tag doesn't point at a clean, distinct commit -- check 'git describe --tags --long' and 'git status'), not uploading to pypi"
|
|
28
|
+
rm -r dist
|
|
29
|
+
rm -r src/fpathlib.egg-info
|
|
30
|
+
return
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
twine upload --verbose dist/*
|
|
34
|
+
rm -r dist
|
|
35
|
+
rm -r src/fpathlib.egg-info
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Bumps the version tag and pushes it. This MUST leave HEAD on a commit that
|
|
2
|
+
# is not shared with any older tag, or setuptools_scm can't tell the new tag
|
|
3
|
+
# apart from the old one and pypi.sh will build a .devN version instead of a
|
|
4
|
+
# clean release (see the 0.1.3/0.1.2 incident: both tags landed on the same
|
|
5
|
+
# commit because there was nothing new to commit, so setuptools_scm treated
|
|
6
|
+
# the checkout as ambiguous).
|
|
7
|
+
set -e
|
|
8
|
+
|
|
9
|
+
m=$1
|
|
10
|
+
|
|
11
|
+
tag=$(git describe --tags --abbrev=0)
|
|
12
|
+
parts=(${tag//./ })
|
|
13
|
+
if [ "$m" == "major" ]
|
|
14
|
+
then
|
|
15
|
+
parts[0]=$((parts[0]+1))
|
|
16
|
+
parts[1]=0
|
|
17
|
+
parts[2]=0
|
|
18
|
+
elif [ "$m" == "minor" ]
|
|
19
|
+
then
|
|
20
|
+
parts[1]=$((parts[1]+1))
|
|
21
|
+
parts[2]=0
|
|
22
|
+
elif [ "$m" == "patch" ]
|
|
23
|
+
then
|
|
24
|
+
parts[2]=$((parts[2]+1))
|
|
25
|
+
else
|
|
26
|
+
echo "must specify major, minor, or patch"
|
|
27
|
+
return
|
|
28
|
+
fi
|
|
29
|
+
tag="${parts[0]}.${parts[1]}.${parts[2]}"
|
|
30
|
+
|
|
31
|
+
# Fail loudly instead of leaving a stale/misplaced tag if $tag already exists
|
|
32
|
+
# (locally or on origin) -- creating it again would either error out partway
|
|
33
|
+
# through or, worse, silently retag the wrong commit.
|
|
34
|
+
if git rev-parse -q --verify "refs/tags/$tag" >/dev/null
|
|
35
|
+
then
|
|
36
|
+
echo "tag $tag already exists locally -- delete it first (git tag -d $tag) if you really want to redo this release"
|
|
37
|
+
return
|
|
38
|
+
fi
|
|
39
|
+
if git ls-remote --exit-code --tags origin "$tag" >/dev/null 2>&1
|
|
40
|
+
then
|
|
41
|
+
echo "tag $tag already exists on origin -- delete it first (git push origin :refs/tags/$tag) if you really want to redo this release"
|
|
42
|
+
return
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
git add -A
|
|
46
|
+
# --allow-empty is required: if there's nothing to commit (working tree
|
|
47
|
+
# already clean), a plain `git commit` would no-op and this tag would end up
|
|
48
|
+
# pointing at the SAME commit as the previous tag, confusing setuptools_scm.
|
|
49
|
+
git commit --allow-empty -m "tag $tag"
|
|
50
|
+
git push origin main
|
|
51
|
+
|
|
52
|
+
git checkout main
|
|
53
|
+
git pull origin main
|
|
54
|
+
|
|
55
|
+
git tag $tag
|
|
56
|
+
git push origin $tag
|
|
@@ -2,6 +2,7 @@ from fpathlib.path import Path
|
|
|
2
2
|
from fpathlib.fpath import FPath, ExpandedFPath
|
|
3
3
|
from fpathlib.expand import (
|
|
4
4
|
expand_fpath,
|
|
5
|
+
iexpand_fpath,
|
|
5
6
|
expand_fpath_decorator,
|
|
6
7
|
is_expandable,
|
|
7
8
|
)
|
|
@@ -11,6 +12,7 @@ __all__ = [
|
|
|
11
12
|
"FPath",
|
|
12
13
|
"ExpandedFPath",
|
|
13
14
|
"expand_fpath",
|
|
15
|
+
"iexpand_fpath",
|
|
14
16
|
"expand_fpath_decorator",
|
|
15
17
|
"is_expandable",
|
|
16
18
|
]
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.1.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 1,
|
|
21
|
+
__version__ = version = '0.1.4'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 4)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'gd0a205773'
|
|
@@ -31,6 +31,38 @@ def expand_fpath(fpath, *, exclude_path_patterns=None, require_metadata=True):
|
|
|
31
31
|
)
|
|
32
32
|
|
|
33
33
|
|
|
34
|
+
def iexpand_fpath(fpath, *, exclude_path_patterns=None, require_metadata=True, errors="raise"):
|
|
35
|
+
"""
|
|
36
|
+
Generator equivalent of :func:`.expand_fpath` -- lazily yields each
|
|
37
|
+
matching :obj:`.Path` instead of building the whole
|
|
38
|
+
:obj:`.ExpandedFPath` up front. This is a convenience function that
|
|
39
|
+
simply creates an :obj:`FPath` and calls its :meth:`.FPath.iexpand`
|
|
40
|
+
method.
|
|
41
|
+
|
|
42
|
+
Parameters
|
|
43
|
+
----------
|
|
44
|
+
fpath : :obj:`str`
|
|
45
|
+
An f-string path, where the variables are captured and stored along the path
|
|
46
|
+
name.
|
|
47
|
+
exclude_path_patterns : :obj:`str` or :obj:`Iterable`[:obj:`str`]
|
|
48
|
+
Exclude paths that match the supplied pattern. (Default: None).
|
|
49
|
+
require_metadata : :obj:`bool`
|
|
50
|
+
Require that all paths identified must have found metadata. (Default: True).
|
|
51
|
+
errors : :obj:`str`
|
|
52
|
+
How to handle the "no matches" case. See :meth:`.FPath.iexpand`. (Default: "raise").
|
|
53
|
+
|
|
54
|
+
Yields
|
|
55
|
+
------
|
|
56
|
+
:obj:`.Path`
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
return FPath(fpath).iexpand(
|
|
60
|
+
exclude_path_patterns=exclude_path_patterns,
|
|
61
|
+
require_metadata=require_metadata,
|
|
62
|
+
errors=errors,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
34
66
|
def expand_fpath_decorator(f=None, require_expandable=True, post_process=None):
|
|
35
67
|
"""
|
|
36
68
|
Decorator for :func:`.expand_fpath`.
|
|
@@ -88,7 +120,8 @@ def expand_fpath_decorator(f=None, require_expandable=True, post_process=None):
|
|
|
88
120
|
|
|
89
121
|
def is_expandable(fpath):
|
|
90
122
|
"""
|
|
91
|
-
Check
|
|
123
|
+
Check whether `fpath` has fpathlib's own named f-string captures (e.g.
|
|
124
|
+
`{name}`).
|
|
92
125
|
|
|
93
126
|
Parameters
|
|
94
127
|
----------
|
|
@@ -169,6 +169,7 @@ def scan_txt(
|
|
|
169
169
|
has_header=False,
|
|
170
170
|
keep_line=False,
|
|
171
171
|
usecols=None,
|
|
172
|
+
validate_schema=True,
|
|
172
173
|
*args,
|
|
173
174
|
**kwargs,
|
|
174
175
|
):
|
|
@@ -198,6 +199,17 @@ def scan_txt(
|
|
|
198
199
|
Whether to keep the original line as a column in the output.
|
|
199
200
|
usecols : :obj:`list`[:obj:`int`], optional
|
|
200
201
|
Indexes of columns to keep in the output. If not provided, all columns are kept. Only applicable if `separator` is provided.
|
|
202
|
+
validate_schema : :obj:`bool`
|
|
203
|
+
When the field count is inferred from a single representative file
|
|
204
|
+
(the fast path for a multi-file glob/pattern), also check that no
|
|
205
|
+
*other* matched file has *more* fields than that sample. A file with
|
|
206
|
+
fewer fields than the sample already raises a clear polars error
|
|
207
|
+
(out-of-bounds list access); a file with more fields would otherwise
|
|
208
|
+
have its extra columns silently dropped instead of erroring. This
|
|
209
|
+
check reads every matched file's line count, which costs an extra
|
|
210
|
+
full pass over the data on top of the fast path -- pass False if
|
|
211
|
+
you've already confirmed your files are consistent and want to
|
|
212
|
+
skip it for speed on very large globs. (Default: True)
|
|
201
213
|
*args
|
|
202
214
|
Positional arguments to pass to :meth:`polars.scan_csv`.
|
|
203
215
|
**kwargs
|
|
@@ -282,6 +294,26 @@ def scan_txt(
|
|
|
282
294
|
# Initial field names, may be renamed later from header or by `new_columns`
|
|
283
295
|
fields = {i: f"field_{i}" for i in range(n_fields)}
|
|
284
296
|
|
|
297
|
+
# n_fields came from a single sample file (see above), so a file
|
|
298
|
+
# with *fewer* fields than the sample will already raise a clear
|
|
299
|
+
# polars error below (list.get() on an out-of-bounds index). A
|
|
300
|
+
# file with *more* fields would not -- its extra columns would
|
|
301
|
+
# just be silently dropped -- so check for that explicitly if
|
|
302
|
+
# requested.
|
|
303
|
+
if validate_schema and sample_schema is not None:
|
|
304
|
+
max_fields = (
|
|
305
|
+
lf.select(_polars.col("fields").list.len().max())
|
|
306
|
+
.collect()
|
|
307
|
+
.item()
|
|
308
|
+
)
|
|
309
|
+
if max_fields is not None and max_fields > n_fields:
|
|
310
|
+
msg = (
|
|
311
|
+
f"field count mismatch: schema was inferred from a "
|
|
312
|
+
f"single sample file with {n_fields} fields, but at "
|
|
313
|
+
f"least one matched file has {max_fields} fields"
|
|
314
|
+
)
|
|
315
|
+
raise ValueError(msg)
|
|
316
|
+
|
|
285
317
|
# Add each field as a separate column
|
|
286
318
|
for i, field in fields.items():
|
|
287
319
|
lf = lf.with_columns(_polars.col("fields").list.get(i).alias(field))
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from collections.abc import Sequence
|
|
2
|
-
from glob import glob
|
|
2
|
+
from glob import glob, iglob
|
|
3
3
|
import parse
|
|
4
4
|
import re
|
|
5
5
|
|
|
@@ -31,10 +31,19 @@ class FPath:
|
|
|
31
31
|
def __repr__(self):
|
|
32
32
|
return "FPath({!r})".format(self.fpath)
|
|
33
33
|
|
|
34
|
-
def
|
|
34
|
+
def iexpand(self, exclude_path_patterns=None, require_metadata=True, errors="raise"):
|
|
35
35
|
"""
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
Lazily yield each :obj:`.Path` matching the f-string pattern, one at
|
|
37
|
+
a time, instead of building the whole :obj:`.ExpandedFPath` up
|
|
38
|
+
front. Useful when a pattern could match a very large number of
|
|
39
|
+
files and you don't want them all held in memory at once, or want
|
|
40
|
+
to start processing before the full glob finishes walking the
|
|
41
|
+
filesystem. :meth:`.expand` is built on top of this generator.
|
|
42
|
+
|
|
43
|
+
Like any generator function, nothing in the body runs -- including
|
|
44
|
+
argument validation and the wildcard/{} capture check -- until the
|
|
45
|
+
result is first iterated (a `for` loop, `list(...)`, etc.), not at
|
|
46
|
+
the moment :meth:`.iexpand` is called.
|
|
38
47
|
|
|
39
48
|
Parameters
|
|
40
49
|
----------
|
|
@@ -43,13 +52,13 @@ class FPath:
|
|
|
43
52
|
require_metadata : :obj:`bool`
|
|
44
53
|
Require that all paths identified must have found metadata. (Default: True).
|
|
45
54
|
errors : :obj:`str`
|
|
46
|
-
How to handle
|
|
47
|
-
|
|
48
|
-
|
|
55
|
+
How to handle the "no matches" case, checked once the pattern is
|
|
56
|
+
fully exhausted. If "raise", then raise an error. If "warn",
|
|
57
|
+
then warn. If "ignore", then do nothing. (Default: "raise").
|
|
49
58
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
:obj:`.
|
|
59
|
+
Yields
|
|
60
|
+
------
|
|
61
|
+
:obj:`.Path`
|
|
53
62
|
"""
|
|
54
63
|
|
|
55
64
|
if errors not in {"raise", "warn", "ignore"}:
|
|
@@ -58,8 +67,34 @@ class FPath:
|
|
|
58
67
|
|
|
59
68
|
parser = parse.compile(self.fpath)
|
|
60
69
|
|
|
61
|
-
|
|
62
|
-
|
|
70
|
+
# glob() (used below to find files) and `parse` (used above to
|
|
71
|
+
# extract {name} values) disagree about what a bare '*', '?', or
|
|
72
|
+
# '[...]' means outside a {} capture: glob treats it as a wildcard,
|
|
73
|
+
# but parse's format-string language only special-cases {...} and
|
|
74
|
+
# reads everything else -- including '*' -- as literal text to
|
|
75
|
+
# match. A pattern like "{a}/*" would find files fine via glob()
|
|
76
|
+
# but then fail to parse against that same string, since real
|
|
77
|
+
# filenames don't literally contain "/*" -- silently producing
|
|
78
|
+
# metadata=None for every match, or raising a confusing "metadata
|
|
79
|
+
# not found" error, with no indication that '*' was the actual
|
|
80
|
+
# problem. Fail fast and explain it instead. Only applies when
|
|
81
|
+
# there's a {} capture to begin with -- a plain glob with none is a
|
|
82
|
+
# separate, unaffected case (no metadata is expected from it).
|
|
83
|
+
if parser.named_fields:
|
|
84
|
+
literal_fpath = re.sub(r"\{.*?\}", "", self.fpath)
|
|
85
|
+
for c in "*?[":
|
|
86
|
+
if c in literal_fpath:
|
|
87
|
+
msg = (
|
|
88
|
+
f"glob wildcard {c!r} outside a {{}} capture is not "
|
|
89
|
+
f"supported in {self.fpath!r} -- 'parse' treats it "
|
|
90
|
+
"as a literal character, not a wildcard. Use a "
|
|
91
|
+
"named capture (e.g. '{a}/{b}') if you want that "
|
|
92
|
+
"segment captured too."
|
|
93
|
+
)
|
|
94
|
+
raise ValueError(msg)
|
|
95
|
+
|
|
96
|
+
n = 0
|
|
97
|
+
for fname in iglob(re.sub(r"\{.*?\}", "*", self.fpath)):
|
|
63
98
|
path = Path(fname)
|
|
64
99
|
if exclude_path_patterns and path.match_any(exclude_path_patterns):
|
|
65
100
|
continue
|
|
@@ -67,9 +102,10 @@ class FPath:
|
|
|
67
102
|
if require_metadata and path.metadata is None:
|
|
68
103
|
msg = f"metadata not found for '{fname}' with '{self.fpath}'"
|
|
69
104
|
raise AttributeError(msg)
|
|
70
|
-
|
|
105
|
+
n += 1
|
|
106
|
+
yield path
|
|
71
107
|
|
|
72
|
-
if
|
|
108
|
+
if n == 0 and errors != "ignore":
|
|
73
109
|
msg = f"no paths found for {self.fpath.__repr__()}"
|
|
74
110
|
if errors == "raise":
|
|
75
111
|
raise IOError(msg)
|
|
@@ -78,6 +114,35 @@ class FPath:
|
|
|
78
114
|
|
|
79
115
|
warnings.warn(msg)
|
|
80
116
|
|
|
117
|
+
def expand(self, exclude_path_patterns=None, require_metadata=True, errors="raise"):
|
|
118
|
+
"""
|
|
119
|
+
Use an f-string to extract out a collection of paths, where the f-string
|
|
120
|
+
variables are captured and stored along the path name.
|
|
121
|
+
|
|
122
|
+
Parameters
|
|
123
|
+
----------
|
|
124
|
+
exclude_path_patterns : :obj:`str` or :obj:`Iterable`[:obj:`str`]
|
|
125
|
+
Exclude paths that match the supplied pattern. (Default: None).
|
|
126
|
+
require_metadata : :obj:`bool`
|
|
127
|
+
Require that all paths identified must have found metadata. (Default: True).
|
|
128
|
+
errors : :obj:`str`
|
|
129
|
+
How to handle errors. If "raise", then raise an error. If "warn", then warn
|
|
130
|
+
and return an empty collection. If "ignore", then ignore the error and
|
|
131
|
+
return an empty collection. (Default: "raise").
|
|
132
|
+
|
|
133
|
+
Returns
|
|
134
|
+
-------
|
|
135
|
+
:obj:`.ExpandedFPath`
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
paths = list(
|
|
139
|
+
self.iexpand(
|
|
140
|
+
exclude_path_patterns=exclude_path_patterns,
|
|
141
|
+
require_metadata=require_metadata,
|
|
142
|
+
errors=errors,
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
|
|
81
146
|
return ExpandedFPath(paths=paths, fpath=self)
|
|
82
147
|
|
|
83
148
|
|
|
@@ -153,7 +218,13 @@ class ExpandedFPath(Sequence):
|
|
|
153
218
|
|
|
154
219
|
pl = import_optional_dependency("polars")
|
|
155
220
|
|
|
156
|
-
|
|
221
|
+
# `value` is None whenever this ExpandedFPath was built with
|
|
222
|
+
# require_metadata=False and a path's metadata simply wasn't found
|
|
223
|
+
# (e.g. a plain glob with no named captures at all) -- treat that as
|
|
224
|
+
# "no metadata columns" rather than crashing on `**None`.
|
|
225
|
+
data = [
|
|
226
|
+
{"fname": str(key), **(value or {})} for key, value in self.metadata.items()
|
|
227
|
+
]
|
|
157
228
|
df = pl.DataFrame(data)
|
|
158
229
|
if lazy:
|
|
159
230
|
df = df.lazy()
|
|
@@ -4,11 +4,13 @@ MANIFEST.in
|
|
|
4
4
|
README.md
|
|
5
5
|
TODO.txt
|
|
6
6
|
pyproject.toml
|
|
7
|
+
conda-recipe/meta.yaml
|
|
7
8
|
docs/Makefile
|
|
8
9
|
docs/make.bat
|
|
9
10
|
docs/source/api.rst
|
|
10
11
|
docs/source/conf.py
|
|
11
12
|
docs/source/index.rst
|
|
13
|
+
scripts/conda.sh
|
|
12
14
|
scripts/deploy.sh
|
|
13
15
|
scripts/docs.sh
|
|
14
16
|
scripts/pypi.sh
|
|
@@ -5,12 +5,14 @@
|
|
|
5
5
|
"MANIFEST.in",
|
|
6
6
|
"README.md",
|
|
7
7
|
"TODO.txt",
|
|
8
|
+
"conda-recipe/meta.yaml",
|
|
8
9
|
"docs/Makefile",
|
|
9
10
|
"docs/make.bat",
|
|
10
11
|
"docs/source/api.rst",
|
|
11
12
|
"docs/source/conf.py",
|
|
12
13
|
"docs/source/index.rst",
|
|
13
14
|
"pyproject.toml",
|
|
15
|
+
"scripts/conda.sh",
|
|
14
16
|
"scripts/deploy.sh",
|
|
15
17
|
"scripts/docs.sh",
|
|
16
18
|
"scripts/pypi.sh",
|
fpathlib-0.1.3/scripts/deploy.sh
DELETED
fpathlib-0.1.3/scripts/pypi.sh
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
allow_dev=0
|
|
2
|
-
if [ "$1" = "dev" ]
|
|
3
|
-
then
|
|
4
|
-
allow_dev=1
|
|
5
|
-
fi
|
|
6
|
-
|
|
7
|
-
python3 -m build
|
|
8
|
-
|
|
9
|
-
dev_artifacts=$(ls dist/ | grep -c '\.dev[0-9]')
|
|
10
|
-
if [ $allow_dev -eq 0 ] && [ "$dev_artifacts" != "0" ]
|
|
11
|
-
then
|
|
12
|
-
echo "built version is a dev version (tag likely doesn't point at a distinct commit), not uploading to pypi"
|
|
13
|
-
rm -r dist
|
|
14
|
-
rm -r src/fpathlib.egg-info
|
|
15
|
-
return
|
|
16
|
-
fi
|
|
17
|
-
|
|
18
|
-
twine upload --verbose dist/*
|
|
19
|
-
rm -r dist
|
|
20
|
-
rm -r src/fpathlib.egg-info
|
fpathlib-0.1.3/scripts/tag.sh
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
set -e
|
|
2
|
-
|
|
3
|
-
m=$1
|
|
4
|
-
|
|
5
|
-
tag=$(git describe --tags --abbrev=0)
|
|
6
|
-
parts=(${tag//./ })
|
|
7
|
-
if [ "$m" == "major" ]
|
|
8
|
-
then
|
|
9
|
-
parts[0]=$((parts[0]+1))
|
|
10
|
-
parts[1]=0
|
|
11
|
-
parts[2]=0
|
|
12
|
-
elif [ "$m" == "minor" ]
|
|
13
|
-
then
|
|
14
|
-
parts[1]=$((parts[1]+1))
|
|
15
|
-
parts[2]=0
|
|
16
|
-
elif [ "$m" == "patch" ]
|
|
17
|
-
then
|
|
18
|
-
parts[2]=$((parts[2]+1))
|
|
19
|
-
else
|
|
20
|
-
echo "must specify major, minor, or patch"
|
|
21
|
-
return
|
|
22
|
-
fi
|
|
23
|
-
tag="${parts[0]}.${parts[1]}.${parts[2]}"
|
|
24
|
-
|
|
25
|
-
git add -A
|
|
26
|
-
git commit --allow-empty -m "tag $tag"
|
|
27
|
-
git push origin main
|
|
28
|
-
|
|
29
|
-
git checkout main
|
|
30
|
-
git pull origin main
|
|
31
|
-
|
|
32
|
-
git tag $tag
|
|
33
|
-
git push origin $tag
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|