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.
Files changed (38) hide show
  1. {fpathlib-0.1.3 → fpathlib-0.1.4}/.gitignore +1 -0
  2. {fpathlib-0.1.3/src/fpathlib.egg-info → fpathlib-0.1.4}/PKG-INFO +1 -1
  3. fpathlib-0.1.4/conda-recipe/meta.yaml +39 -0
  4. fpathlib-0.1.4/scripts/conda.sh +50 -0
  5. fpathlib-0.1.4/scripts/deploy.sh +13 -0
  6. fpathlib-0.1.4/scripts/pypi.sh +35 -0
  7. fpathlib-0.1.4/scripts/tag.sh +56 -0
  8. {fpathlib-0.1.3 → fpathlib-0.1.4}/src/fpathlib/__init__.py +2 -0
  9. {fpathlib-0.1.3 → fpathlib-0.1.4}/src/fpathlib/_version.py +3 -3
  10. {fpathlib-0.1.3 → fpathlib-0.1.4}/src/fpathlib/expand.py +34 -1
  11. {fpathlib-0.1.3 → fpathlib-0.1.4}/src/fpathlib/ext/polars.py +32 -0
  12. {fpathlib-0.1.3 → fpathlib-0.1.4}/src/fpathlib/fpath.py +86 -15
  13. {fpathlib-0.1.3 → fpathlib-0.1.4/src/fpathlib.egg-info}/PKG-INFO +1 -1
  14. {fpathlib-0.1.3 → fpathlib-0.1.4}/src/fpathlib.egg-info/SOURCES.txt +2 -0
  15. {fpathlib-0.1.3 → fpathlib-0.1.4}/src/fpathlib.egg-info/scm_file_list.json +2 -0
  16. fpathlib-0.1.4/src/fpathlib.egg-info/scm_version.json +8 -0
  17. fpathlib-0.1.3/scripts/deploy.sh +0 -3
  18. fpathlib-0.1.3/scripts/pypi.sh +0 -20
  19. fpathlib-0.1.3/scripts/tag.sh +0 -33
  20. fpathlib-0.1.3/src/fpathlib.egg-info/scm_version.json +0 -8
  21. {fpathlib-0.1.3 → fpathlib-0.1.4}/LICENSE +0 -0
  22. {fpathlib-0.1.3 → fpathlib-0.1.4}/MANIFEST.in +0 -0
  23. {fpathlib-0.1.3 → fpathlib-0.1.4}/README.md +0 -0
  24. {fpathlib-0.1.3 → fpathlib-0.1.4}/TODO.txt +0 -0
  25. {fpathlib-0.1.3 → fpathlib-0.1.4}/docs/Makefile +0 -0
  26. {fpathlib-0.1.3 → fpathlib-0.1.4}/docs/make.bat +0 -0
  27. {fpathlib-0.1.3 → fpathlib-0.1.4}/docs/source/api.rst +0 -0
  28. {fpathlib-0.1.3 → fpathlib-0.1.4}/docs/source/conf.py +0 -0
  29. {fpathlib-0.1.3 → fpathlib-0.1.4}/docs/source/index.rst +0 -0
  30. {fpathlib-0.1.3 → fpathlib-0.1.4}/pyproject.toml +0 -0
  31. {fpathlib-0.1.3 → fpathlib-0.1.4}/scripts/docs.sh +0 -0
  32. {fpathlib-0.1.3 → fpathlib-0.1.4}/setup.cfg +0 -0
  33. {fpathlib-0.1.3 → fpathlib-0.1.4}/src/fpathlib/ext/__init__.py +0 -0
  34. {fpathlib-0.1.3 → fpathlib-0.1.4}/src/fpathlib/path.py +0 -0
  35. {fpathlib-0.1.3 → fpathlib-0.1.4}/src/fpathlib/utils.py +0 -0
  36. {fpathlib-0.1.3 → fpathlib-0.1.4}/src/fpathlib.egg-info/dependency_links.txt +0 -0
  37. {fpathlib-0.1.3 → fpathlib-0.1.4}/src/fpathlib.egg-info/requires.txt +0 -0
  38. {fpathlib-0.1.3 → fpathlib-0.1.4}/src/fpathlib.egg-info/top_level.txt +0 -0
@@ -5,3 +5,4 @@ docs/build
5
5
  dist
6
6
  build
7
7
  src/fpathlib/_version.py
8
+ *.swp
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fpathlib
3
- Version: 0.1.3
3
+ Version: 0.1.4
4
4
  Summary: A package to combine paths with metadata
5
5
  Author-email: "C. Lockhart" <clockha2@gmu.edu>
6
6
  Requires-Python: >=3.12
@@ -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.3'
22
- __version_tuple__ = version_tuple = (0, 1, 3)
21
+ __version__ = version = '0.1.4'
22
+ __version_tuple__ = version_tuple = (0, 1, 4)
23
23
 
24
- __commit_id__ = commit_id = 'g02b650d92'
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 if expandable.
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 expand(self, exclude_path_patterns=None, require_metadata=True, errors="raise"):
34
+ def iexpand(self, exclude_path_patterns=None, require_metadata=True, errors="raise"):
35
35
  """
36
- Use an f-string to extract out a collection of paths, where the f-string
37
- variables are captured and stored along the path name.
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 errors. If "raise", then raise an error. If "warn", then warn
47
- and return an empty collection. If "ignore", then ignore the error and
48
- return an empty collection. (Default: "raise").
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
- Returns
51
- -------
52
- :obj:`.ExpandedFPath`
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
- paths = []
62
- for fname in glob(re.sub(r"\{.*?\}", "*", self.fpath)):
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
- paths.append(path)
105
+ n += 1
106
+ yield path
71
107
 
72
- if len(paths) == 0 and errors != "ignore":
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
- data = [{"fname": str(key), **value} for key, value in self.metadata.items()]
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()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fpathlib
3
- Version: 0.1.3
3
+ Version: 0.1.4
4
4
  Summary: A package to combine paths with metadata
5
5
  Author-email: "C. Lockhart" <clockha2@gmu.edu>
6
6
  Requires-Python: >=3.12
@@ -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",
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "0.1.4",
3
+ "distance": 0,
4
+ "node": "gd0a205773124033d6ce5a47583a001bddacc729f",
5
+ "dirty": false,
6
+ "branch": "main",
7
+ "node_date": "2026-09-17"
8
+ }
@@ -1,3 +0,0 @@
1
- source scripts/docs.sh
2
- source scripts/tag.sh "patch"
3
- source scripts/pypi.sh
@@ -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
@@ -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
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "0.1.3",
3
- "distance": 0,
4
- "node": "g02b650d9221c0a98fa837aae39d499573ad9b4ad",
5
- "dirty": false,
6
- "branch": "main",
7
- "node_date": "2026-09-15"
8
- }
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