dotted-notation 0.44.2__tar.gz → 0.44.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.
- {dotted_notation-0.44.2 → dotted_notation-0.44.4}/CHANGELOG.md +28 -0
- {dotted_notation-0.44.2/dotted_notation.egg-info → dotted_notation-0.44.4}/PKG-INFO +82 -5
- {dotted_notation-0.44.2 → dotted_notation-0.44.4}/README.md +81 -4
- {dotted_notation-0.44.2 → dotted_notation-0.44.4}/dotted/__init__.py +2 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.4}/dotted/api.py +32 -13
- {dotted_notation-0.44.2 → dotted_notation-0.44.4}/dotted/base.py +2 -2
- {dotted_notation-0.44.2 → dotted_notation-0.44.4}/dotted/engine.py +42 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.4}/dotted/matchers.py +9 -2
- {dotted_notation-0.44.2 → dotted_notation-0.44.4}/dotted/results.py +32 -5
- {dotted_notation-0.44.2 → dotted_notation-0.44.4}/dotted/utils.py +19 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.4/dotted_notation.egg-info}/PKG-INFO +82 -5
- {dotted_notation-0.44.2 → dotted_notation-0.44.4}/pyproject.toml +1 -1
- {dotted_notation-0.44.2 → dotted_notation-0.44.4}/LICENSE +0 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.4}/MANIFEST.in +0 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.4}/dotted/__main__.py +0 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.4}/dotted/access.py +0 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.4}/dotted/cli/__init__.py +0 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.4}/dotted/cli/_compat.py +0 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.4}/dotted/cli/formats.py +0 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.4}/dotted/cli/main.py +0 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.4}/dotted/containers.py +0 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.4}/dotted/filters.py +0 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.4}/dotted/grammar.py +0 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.4}/dotted/groups.py +0 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.4}/dotted/predicates.py +0 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.4}/dotted/recursive.py +0 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.4}/dotted/sql/__init__.py +0 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.4}/dotted/sql/core.py +0 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.4}/dotted/sql/pg.py +0 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.4}/dotted/transforms.py +0 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.4}/dotted/utypes.py +0 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.4}/dotted/wrappers.py +0 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.4}/dotted_notation.egg-info/SOURCES.txt +0 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.4}/dotted_notation.egg-info/dependency_links.txt +0 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.4}/dotted_notation.egg-info/entry_points.txt +0 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.4}/dotted_notation.egg-info/requires.txt +0 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.4}/dotted_notation.egg-info/top_level.txt +0 -0
- {dotted_notation-0.44.2 → dotted_notation-0.44.4}/setup.cfg +0 -0
|
@@ -3,6 +3,34 @@
|
|
|
3
3
|
All notable changes to `dotted` are recorded here. Versions prior to
|
|
4
4
|
the ones listed are omitted — browse git history for earlier entries.
|
|
5
5
|
|
|
6
|
+
## [0.44.4]
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
- Module-level escape hatches: `dotted.set_simple_fastpath(False)` disables
|
|
10
|
+
the get() fast path (everything goes through walk()), and
|
|
11
|
+
`dotted.set_parse_cache(size)` resizes the LRU cache behind parse()
|
|
12
|
+
(0 disables, None unbounded). Both return the previous setting.
|
|
13
|
+
|
|
14
|
+
### Performance
|
|
15
|
+
- `get()` fast path for simple paths (#58): a plain chain of literal
|
|
16
|
+
Key/Attr/Slot accesses (`a.b`, `a[0].b`, `a@x`) resolves with direct
|
|
17
|
+
dict/list/attr lookups, skipping the walk() machinery entirely. The
|
|
18
|
+
chain is computed once at parse time and cached on the `Dotted`
|
|
19
|
+
(`simple_chain`); unusual containers (dict subclasses, custom
|
|
20
|
+
mappings) fall back to the full traversal.
|
|
21
|
+
- `Const.value`/`Numeric.value` are now computed once and cached on the
|
|
22
|
+
instance instead of recomputed per property access.
|
|
23
|
+
- `Dotted.__hash__` is cached, making repeated cache lookups keyed on a
|
|
24
|
+
pre-parsed `Dotted` (e.g. `get(obj, parsed)`) much cheaper.
|
|
25
|
+
|
|
26
|
+
## [0.44.3]
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
- Transforms with a dict (or other nested-container) argument, e.g.
|
|
30
|
+
`code|lookup:{"a": 1}`, no longer raise `TypeError: unhashable type: 'dict'`
|
|
31
|
+
when the path is parsed. `Transform.__hash__` now freezes container params
|
|
32
|
+
recursively.
|
|
33
|
+
|
|
6
34
|
## [0.44.2]
|
|
7
35
|
|
|
8
36
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dotted_notation
|
|
3
|
-
Version: 0.44.
|
|
3
|
+
Version: 0.44.4
|
|
4
4
|
Summary: Dotted notation for safe nested data traversal with optional chaining, pattern matching, and transforms
|
|
5
5
|
Author-email: Frey Waid <logophage1@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -176,6 +176,7 @@ Or pick only what you need:
|
|
|
176
176
|
- [Projection](#projection)
|
|
177
177
|
- [Unpack](#unpack)
|
|
178
178
|
- [Pack](#pack)
|
|
179
|
+
- [Recipes](#recipes)
|
|
179
180
|
- [FAQ](#faq)
|
|
180
181
|
- [Why do I get a tuple for my get?](#why-do-i-get-a-tuple-for-my-get)
|
|
181
182
|
- [How do I craft an efficient path?](#how-do-i-craft-an-efficient-path)
|
|
@@ -801,10 +802,13 @@ normal form. All three call `unpack()` internally.
|
|
|
801
802
|
>>> dotted.keys({'a': 1, 'b': 2}) & dotted.keys({'b': 3, 'c': 4})
|
|
802
803
|
{'b'}
|
|
803
804
|
|
|
804
|
-
All three accept `attrs=`
|
|
805
|
+
All three accept the same `attrs=`, `project=`, and `partial=` arguments as
|
|
806
|
+
`unpack`:
|
|
805
807
|
|
|
806
808
|
>>> dotted.keys({'point': Pt(3, 4)}, attrs=[dotted.Attrs.standard])
|
|
807
809
|
dict_keys(['point@x', 'point@y'])
|
|
810
|
+
>>> dotted.items({'a': {'b': 1, 'c': 2}, 'x': 3}, project='a')
|
|
811
|
+
dict_items([('a.b', 1), ('a.c', 2)])
|
|
808
812
|
|
|
809
813
|
<a id="build"></a>
|
|
810
814
|
### Build
|
|
@@ -2956,7 +2960,7 @@ Fragments concatenate naturally via `+` / `__radd__`; metadata merges:
|
|
|
2956
2960
|
>>> r = dotted.sqlize("age >= $(min_age)", driver='asyncpg')
|
|
2957
2961
|
>>> combined = "WHERE " + r.where
|
|
2958
2962
|
>>> r.build(combined, min_age=30)
|
|
2959
|
-
('WHERE age >= $1
|
|
2963
|
+
('WHERE age >= $1', [30])
|
|
2960
2964
|
|
|
2961
2965
|
### Hoisted params
|
|
2962
2966
|
|
|
@@ -3120,7 +3124,7 @@ Pass a shared `ParamPool` to every `sqlize()` call that should compose:
|
|
|
3120
3124
|
>>> r2 = dotted.sqlize('age >= 30', driver='asyncpg', pool=pool)
|
|
3121
3125
|
>>> combined = '(' + r1.where + ') AND (' + r2.where + ')'
|
|
3122
3126
|
>>> dotted.Resolver.build(combined, paramstyle='dollar-numeric')
|
|
3123
|
-
('(status = $1) AND (age
|
|
3127
|
+
('(status = $1) AND (age >= $2)', ['active', 30])
|
|
3124
3128
|
|
|
3125
3129
|
Substitutions by the same original name dedup across Resolvers sharing
|
|
3126
3130
|
a pool — one slot, one value, back-referenced in the rendered SQL:
|
|
@@ -3177,7 +3181,7 @@ segment before building a `Raw`:
|
|
|
3177
3181
|
Col('matched.customer')
|
|
3178
3182
|
>>> Col('schema', 'table', 'col')
|
|
3179
3183
|
Col('schema.table.col')
|
|
3180
|
-
>>> Col('bad; DROP TABLE')
|
|
3184
|
+
>>> Col('bad; DROP TABLE') # doctest: +IGNORE_EXCEPTION_DETAIL
|
|
3181
3185
|
Traceback (most recent call last):
|
|
3182
3186
|
...
|
|
3183
3187
|
dotted.TranslationError: Col part is not a plain identifier: 'bad; DROP TABLE'
|
|
@@ -3423,6 +3427,79 @@ example, removing an entire group without listing every key:
|
|
|
3423
3427
|
echo '{"db.host": "localhost", "db.port": 5432, "app.debug": true}' | dq --pack --unpack remove -p db
|
|
3424
3428
|
# {"app.debug": true}
|
|
3425
3429
|
|
|
3430
|
+
<a id="recipes"></a>
|
|
3431
|
+
## Recipes
|
|
3432
|
+
|
|
3433
|
+
A grab-bag of one-liners that show off what the notation can do. Every example
|
|
3434
|
+
below runs as-is.
|
|
3435
|
+
|
|
3436
|
+
**Flatten a nested list** (leaves only, any depth) — recurse through every slot
|
|
3437
|
+
with `*([*])`, then keep only the deepest match on each branch with `:-1`:
|
|
3438
|
+
|
|
3439
|
+
>>> import dotted
|
|
3440
|
+
>>> dotted.get([1, 2, 3, [4, 5, [6, 7]]], '*([*]):-1')
|
|
3441
|
+
(1, 2, 3, 4, 5, 6, 7)
|
|
3442
|
+
|
|
3443
|
+
Because `:-1` is deepest-*per-branch*, shallow leaves survive alongside deep
|
|
3444
|
+
ones — a ragged list still flattens completely.
|
|
3445
|
+
|
|
3446
|
+
**Collect every leaf value** of a nested dict — same idea with key recursion:
|
|
3447
|
+
|
|
3448
|
+
>>> dotted.get({'a': {'b': 1}, 'c': 2}, '**:-1')
|
|
3449
|
+
(1, 2)
|
|
3450
|
+
|
|
3451
|
+
**Find a key at any depth** — `**` recurses through dict keys, then continue
|
|
3452
|
+
with the key you want:
|
|
3453
|
+
|
|
3454
|
+
>>> dotted.get({'a': {'b': {'name': 'x'}}, 'name': 'y'}, '**.name')
|
|
3455
|
+
('x',)
|
|
3456
|
+
|
|
3457
|
+
If the tree mixes lists and dicts, recurse through both with `*(*#, [*])`:
|
|
3458
|
+
|
|
3459
|
+
>>> dotted.get({'kids': [{'name': 'a'}, {'name': 'b'}]}, '*(*#, [*]).name')
|
|
3460
|
+
('a', 'b')
|
|
3461
|
+
|
|
3462
|
+
**Find values matching a condition anywhere** — attach a value guard to the
|
|
3463
|
+
recursive walk:
|
|
3464
|
+
|
|
3465
|
+
>>> dotted.get({'a': {'b': 7, 'c': 3}, 'd': {'e': 9}}, '**>5')
|
|
3466
|
+
(7, 9)
|
|
3467
|
+
|
|
3468
|
+
**Bulk-update everything that matches** — the same pattern drives `update` and
|
|
3469
|
+
`remove`:
|
|
3470
|
+
|
|
3471
|
+
>>> dotted.update({'a': {'b': 7, 'c': 3}, 'd': 7}, '**=7', 99)
|
|
3472
|
+
{'a': {'b': 99, 'c': 3}, 'd': 99}
|
|
3473
|
+
|
|
3474
|
+
**Filter a list of dicts, then project a field** — combine a key-value filter
|
|
3475
|
+
with a continuation:
|
|
3476
|
+
|
|
3477
|
+
>>> users = [{'name': 'x', 'active': True},
|
|
3478
|
+
... {'name': 'y', 'active': False},
|
|
3479
|
+
... {'name': 'z', 'active': True}]
|
|
3480
|
+
>>> dotted.get(users, '[*&active=true].name')
|
|
3481
|
+
('x', 'z')
|
|
3482
|
+
|
|
3483
|
+
**Upsert** — update a list entry if it exists, else append, using cut (`#`) in a
|
|
3484
|
+
disjunction so the first matching branch wins:
|
|
3485
|
+
|
|
3486
|
+
>>> dotted.update({'emails': [{'email': 'a@x'}]},
|
|
3487
|
+
... 'emails[(*&email="a@x"#, +)].email', 'NEW')
|
|
3488
|
+
{'emails': [{'email': 'NEW'}]}
|
|
3489
|
+
>>> dotted.update({'emails': [{'email': 'a@x'}]},
|
|
3490
|
+
... 'emails[(*&email="z@x"#, +)]', {'email': 'z@x'})
|
|
3491
|
+
{'emails': [{'email': 'a@x'}, {'email': 'z@x'}]}
|
|
3492
|
+
|
|
3493
|
+
**Coerce on the way out** — pipe a value through transforms:
|
|
3494
|
+
|
|
3495
|
+
>>> dotted.get({'name': 'bob', 'tags': [1, 2, 3]}, 'name|uppercase')
|
|
3496
|
+
'BOB'
|
|
3497
|
+
>>> dotted.get({'name': 'bob', 'tags': [1, 2, 3]}, 'tags|len')
|
|
3498
|
+
3
|
|
3499
|
+
|
|
3500
|
+
See [Recursive Traversal](#recursive-traversal), [Filters](#filters), and
|
|
3501
|
+
[Transforms](#transforms) for the full story behind each of these.
|
|
3502
|
+
|
|
3426
3503
|
<a id="faq"></a>
|
|
3427
3504
|
## FAQ
|
|
3428
3505
|
|
|
@@ -136,6 +136,7 @@ Or pick only what you need:
|
|
|
136
136
|
- [Projection](#projection)
|
|
137
137
|
- [Unpack](#unpack)
|
|
138
138
|
- [Pack](#pack)
|
|
139
|
+
- [Recipes](#recipes)
|
|
139
140
|
- [FAQ](#faq)
|
|
140
141
|
- [Why do I get a tuple for my get?](#why-do-i-get-a-tuple-for-my-get)
|
|
141
142
|
- [How do I craft an efficient path?](#how-do-i-craft-an-efficient-path)
|
|
@@ -761,10 +762,13 @@ normal form. All three call `unpack()` internally.
|
|
|
761
762
|
>>> dotted.keys({'a': 1, 'b': 2}) & dotted.keys({'b': 3, 'c': 4})
|
|
762
763
|
{'b'}
|
|
763
764
|
|
|
764
|
-
All three accept `attrs=`
|
|
765
|
+
All three accept the same `attrs=`, `project=`, and `partial=` arguments as
|
|
766
|
+
`unpack`:
|
|
765
767
|
|
|
766
768
|
>>> dotted.keys({'point': Pt(3, 4)}, attrs=[dotted.Attrs.standard])
|
|
767
769
|
dict_keys(['point@x', 'point@y'])
|
|
770
|
+
>>> dotted.items({'a': {'b': 1, 'c': 2}, 'x': 3}, project='a')
|
|
771
|
+
dict_items([('a.b', 1), ('a.c', 2)])
|
|
768
772
|
|
|
769
773
|
<a id="build"></a>
|
|
770
774
|
### Build
|
|
@@ -2916,7 +2920,7 @@ Fragments concatenate naturally via `+` / `__radd__`; metadata merges:
|
|
|
2916
2920
|
>>> r = dotted.sqlize("age >= $(min_age)", driver='asyncpg')
|
|
2917
2921
|
>>> combined = "WHERE " + r.where
|
|
2918
2922
|
>>> r.build(combined, min_age=30)
|
|
2919
|
-
('WHERE age >= $1
|
|
2923
|
+
('WHERE age >= $1', [30])
|
|
2920
2924
|
|
|
2921
2925
|
### Hoisted params
|
|
2922
2926
|
|
|
@@ -3080,7 +3084,7 @@ Pass a shared `ParamPool` to every `sqlize()` call that should compose:
|
|
|
3080
3084
|
>>> r2 = dotted.sqlize('age >= 30', driver='asyncpg', pool=pool)
|
|
3081
3085
|
>>> combined = '(' + r1.where + ') AND (' + r2.where + ')'
|
|
3082
3086
|
>>> dotted.Resolver.build(combined, paramstyle='dollar-numeric')
|
|
3083
|
-
('(status = $1) AND (age
|
|
3087
|
+
('(status = $1) AND (age >= $2)', ['active', 30])
|
|
3084
3088
|
|
|
3085
3089
|
Substitutions by the same original name dedup across Resolvers sharing
|
|
3086
3090
|
a pool — one slot, one value, back-referenced in the rendered SQL:
|
|
@@ -3137,7 +3141,7 @@ segment before building a `Raw`:
|
|
|
3137
3141
|
Col('matched.customer')
|
|
3138
3142
|
>>> Col('schema', 'table', 'col')
|
|
3139
3143
|
Col('schema.table.col')
|
|
3140
|
-
>>> Col('bad; DROP TABLE')
|
|
3144
|
+
>>> Col('bad; DROP TABLE') # doctest: +IGNORE_EXCEPTION_DETAIL
|
|
3141
3145
|
Traceback (most recent call last):
|
|
3142
3146
|
...
|
|
3143
3147
|
dotted.TranslationError: Col part is not a plain identifier: 'bad; DROP TABLE'
|
|
@@ -3383,6 +3387,79 @@ example, removing an entire group without listing every key:
|
|
|
3383
3387
|
echo '{"db.host": "localhost", "db.port": 5432, "app.debug": true}' | dq --pack --unpack remove -p db
|
|
3384
3388
|
# {"app.debug": true}
|
|
3385
3389
|
|
|
3390
|
+
<a id="recipes"></a>
|
|
3391
|
+
## Recipes
|
|
3392
|
+
|
|
3393
|
+
A grab-bag of one-liners that show off what the notation can do. Every example
|
|
3394
|
+
below runs as-is.
|
|
3395
|
+
|
|
3396
|
+
**Flatten a nested list** (leaves only, any depth) — recurse through every slot
|
|
3397
|
+
with `*([*])`, then keep only the deepest match on each branch with `:-1`:
|
|
3398
|
+
|
|
3399
|
+
>>> import dotted
|
|
3400
|
+
>>> dotted.get([1, 2, 3, [4, 5, [6, 7]]], '*([*]):-1')
|
|
3401
|
+
(1, 2, 3, 4, 5, 6, 7)
|
|
3402
|
+
|
|
3403
|
+
Because `:-1` is deepest-*per-branch*, shallow leaves survive alongside deep
|
|
3404
|
+
ones — a ragged list still flattens completely.
|
|
3405
|
+
|
|
3406
|
+
**Collect every leaf value** of a nested dict — same idea with key recursion:
|
|
3407
|
+
|
|
3408
|
+
>>> dotted.get({'a': {'b': 1}, 'c': 2}, '**:-1')
|
|
3409
|
+
(1, 2)
|
|
3410
|
+
|
|
3411
|
+
**Find a key at any depth** — `**` recurses through dict keys, then continue
|
|
3412
|
+
with the key you want:
|
|
3413
|
+
|
|
3414
|
+
>>> dotted.get({'a': {'b': {'name': 'x'}}, 'name': 'y'}, '**.name')
|
|
3415
|
+
('x',)
|
|
3416
|
+
|
|
3417
|
+
If the tree mixes lists and dicts, recurse through both with `*(*#, [*])`:
|
|
3418
|
+
|
|
3419
|
+
>>> dotted.get({'kids': [{'name': 'a'}, {'name': 'b'}]}, '*(*#, [*]).name')
|
|
3420
|
+
('a', 'b')
|
|
3421
|
+
|
|
3422
|
+
**Find values matching a condition anywhere** — attach a value guard to the
|
|
3423
|
+
recursive walk:
|
|
3424
|
+
|
|
3425
|
+
>>> dotted.get({'a': {'b': 7, 'c': 3}, 'd': {'e': 9}}, '**>5')
|
|
3426
|
+
(7, 9)
|
|
3427
|
+
|
|
3428
|
+
**Bulk-update everything that matches** — the same pattern drives `update` and
|
|
3429
|
+
`remove`:
|
|
3430
|
+
|
|
3431
|
+
>>> dotted.update({'a': {'b': 7, 'c': 3}, 'd': 7}, '**=7', 99)
|
|
3432
|
+
{'a': {'b': 99, 'c': 3}, 'd': 99}
|
|
3433
|
+
|
|
3434
|
+
**Filter a list of dicts, then project a field** — combine a key-value filter
|
|
3435
|
+
with a continuation:
|
|
3436
|
+
|
|
3437
|
+
>>> users = [{'name': 'x', 'active': True},
|
|
3438
|
+
... {'name': 'y', 'active': False},
|
|
3439
|
+
... {'name': 'z', 'active': True}]
|
|
3440
|
+
>>> dotted.get(users, '[*&active=true].name')
|
|
3441
|
+
('x', 'z')
|
|
3442
|
+
|
|
3443
|
+
**Upsert** — update a list entry if it exists, else append, using cut (`#`) in a
|
|
3444
|
+
disjunction so the first matching branch wins:
|
|
3445
|
+
|
|
3446
|
+
>>> dotted.update({'emails': [{'email': 'a@x'}]},
|
|
3447
|
+
... 'emails[(*&email="a@x"#, +)].email', 'NEW')
|
|
3448
|
+
{'emails': [{'email': 'NEW'}]}
|
|
3449
|
+
>>> dotted.update({'emails': [{'email': 'a@x'}]},
|
|
3450
|
+
... 'emails[(*&email="z@x"#, +)]', {'email': 'z@x'})
|
|
3451
|
+
{'emails': [{'email': 'a@x'}, {'email': 'z@x'}]}
|
|
3452
|
+
|
|
3453
|
+
**Coerce on the way out** — pipe a value through transforms:
|
|
3454
|
+
|
|
3455
|
+
>>> dotted.get({'name': 'bob', 'tags': [1, 2, 3]}, 'name|uppercase')
|
|
3456
|
+
'BOB'
|
|
3457
|
+
>>> dotted.get({'name': 'bob', 'tags': [1, 2, 3]}, 'tags|len')
|
|
3458
|
+
3
|
|
3459
|
+
|
|
3460
|
+
See [Recursive Traversal](#recursive-traversal), [Filters](#filters), and
|
|
3461
|
+
[Transforms](#transforms) for the full story behind each of these.
|
|
3462
|
+
|
|
3386
3463
|
<a id="faq"></a>
|
|
3387
3464
|
## FAQ
|
|
3388
3465
|
|
|
@@ -58,6 +58,7 @@ For full documentation including all options and flags:
|
|
|
58
58
|
from .api import \
|
|
59
59
|
parse, is_pattern, is_template, is_reference, is_indeterminate, is_simple, \
|
|
60
60
|
is_inverted, is_mutable, mutable, quote, ANY, AUTO, Attrs, GroupMode, \
|
|
61
|
+
set_simple_fastpath, set_parse_cache, \
|
|
61
62
|
register, transform, \
|
|
62
63
|
assemble, assemble_multi, \
|
|
63
64
|
build, build_multi, \
|
|
@@ -87,6 +88,7 @@ __all__ = [
|
|
|
87
88
|
'is_pattern', 'is_template', 'is_reference',
|
|
88
89
|
'is_indeterminate', 'is_simple',
|
|
89
90
|
'is_inverted', 'is_mutable', 'mutable',
|
|
91
|
+
'set_simple_fastpath', 'set_parse_cache',
|
|
90
92
|
# SQL
|
|
91
93
|
'sqlize', 'Resolver', 'SQLFragment', 'ParamStyle', 'ParamPool', 'TranslationError',
|
|
92
94
|
# Constants
|
|
@@ -69,6 +69,32 @@ class ParseError(Exception):
|
|
|
69
69
|
|
|
70
70
|
_parse_lock = threading.Lock()
|
|
71
71
|
|
|
72
|
+
_SIMPLE_FASTPATH = True
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def set_simple_fastpath(enable=True):
|
|
76
|
+
"""
|
|
77
|
+
Enable/disable the simple-path fast path in get() (escape hatch: when
|
|
78
|
+
off, every lookup goes through the full walk() traversal). On by
|
|
79
|
+
default. Returns the previous setting.
|
|
80
|
+
"""
|
|
81
|
+
global _SIMPLE_FASTPATH
|
|
82
|
+
prev = _SIMPLE_FASTPATH
|
|
83
|
+
_SIMPLE_FASTPATH = bool(enable)
|
|
84
|
+
return prev
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def set_parse_cache(size=CACHE_SIZE):
|
|
88
|
+
"""
|
|
89
|
+
Resize the LRU cache behind parse(). 0 disables caching (every path
|
|
90
|
+
string is re-parsed from scratch); None makes it unbounded. Resizing
|
|
91
|
+
discards currently cached parses. Returns the previous size.
|
|
92
|
+
"""
|
|
93
|
+
global _parse
|
|
94
|
+
prev = _parse.cache_info().maxsize
|
|
95
|
+
_parse = functools.lru_cache(size)(_parse.__wrapped__)
|
|
96
|
+
return prev
|
|
97
|
+
|
|
72
98
|
|
|
73
99
|
@functools.lru_cache(CACHE_SIZE)
|
|
74
100
|
def _parse(ops):
|
|
@@ -234,16 +260,6 @@ def is_indeterminate(path):
|
|
|
234
260
|
return _is_template(parsed) or _is_reference(parsed)
|
|
235
261
|
|
|
236
262
|
|
|
237
|
-
@functools.lru_cache(CACHE_SIZE)
|
|
238
|
-
def _is_simple(ops):
|
|
239
|
-
for op in ops:
|
|
240
|
-
if not isinstance(op, (access.Key, access.Attr, access.Slot)):
|
|
241
|
-
return False
|
|
242
|
-
if not isinstance(op.op, matchers.Const):
|
|
243
|
-
return False
|
|
244
|
-
return True
|
|
245
|
-
|
|
246
|
-
|
|
247
263
|
def is_simple(path):
|
|
248
264
|
"""
|
|
249
265
|
True if the path is a plain chain of access ops (Key, Attr, Slot)
|
|
@@ -271,9 +287,7 @@ def is_simple(path):
|
|
|
271
287
|
False
|
|
272
288
|
"""
|
|
273
289
|
parsed = path if isinstance(path, results.Dotted) else parse(path)
|
|
274
|
-
|
|
275
|
-
return False
|
|
276
|
-
return _is_simple(parsed)
|
|
290
|
+
return parsed.simple_chain is not None
|
|
277
291
|
|
|
278
292
|
|
|
279
293
|
def is_inverted(path):
|
|
@@ -433,6 +447,11 @@ def get(obj, path, default=None, pattern_default=(), apply_transforms=True, stri
|
|
|
433
447
|
(7,)
|
|
434
448
|
"""
|
|
435
449
|
ops = parse(path, bindings=bindings, partial=False)
|
|
450
|
+
chain = ops.simple_chain if _SIMPLE_FASTPATH else None
|
|
451
|
+
if chain is not None:
|
|
452
|
+
val = engine.simple_get(chain, obj, strict=strict)
|
|
453
|
+
if val is not engine.SIMPLE_BAIL:
|
|
454
|
+
return default if val is base.marker else val
|
|
436
455
|
vals = engine.iter_until_cut(engine.gets(ops, obj, strict=strict))
|
|
437
456
|
if apply_transforms:
|
|
438
457
|
vals = ( ops.apply(v) for v in vals )
|
|
@@ -272,8 +272,8 @@ class Transform(Op):
|
|
|
272
272
|
try:
|
|
273
273
|
return hash(('transform', self.name, self.params))
|
|
274
274
|
except TypeError:
|
|
275
|
-
|
|
276
|
-
|
|
275
|
+
from .results import Dotted
|
|
276
|
+
return hash(('transform', self.name, Dotted._hashable(self.params)))
|
|
277
277
|
|
|
278
278
|
def __eq__(self, other):
|
|
279
279
|
return (isinstance(other, Transform)
|
|
@@ -48,6 +48,48 @@ def build(ops, node, deepcopy=True, **kwargs):
|
|
|
48
48
|
return built or build_default([cur]+ops)
|
|
49
49
|
|
|
50
50
|
|
|
51
|
+
SIMPLE_BAIL = object()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def simple_get(chain, node, strict=False):
|
|
55
|
+
"""
|
|
56
|
+
Fast path for simple paths (see Dotted.simple_chain): follow literal
|
|
57
|
+
keys with direct dict/list/attr access, skipping walk() entirely.
|
|
58
|
+
Returns the found value, base.marker when the path misses, or
|
|
59
|
+
SIMPLE_BAIL when a node's type falls outside the fast path — the
|
|
60
|
+
caller then falls back to the full traversal.
|
|
61
|
+
"""
|
|
62
|
+
for kind, key in chain:
|
|
63
|
+
if kind == 'attr':
|
|
64
|
+
node = getattr(node, key, base.marker)
|
|
65
|
+
if node is base.marker:
|
|
66
|
+
return base.marker
|
|
67
|
+
continue
|
|
68
|
+
t = type(node)
|
|
69
|
+
if t is dict:
|
|
70
|
+
# strict: Slot never coerces to dict keys
|
|
71
|
+
if strict and kind == 'slot':
|
|
72
|
+
return base.marker
|
|
73
|
+
node = node.get(key, base.marker)
|
|
74
|
+
if node is base.marker:
|
|
75
|
+
return base.marker
|
|
76
|
+
continue
|
|
77
|
+
if t is list or t is tuple:
|
|
78
|
+
# strict: Key never coerces to sequence indices
|
|
79
|
+
if strict and kind == 'key':
|
|
80
|
+
return base.marker
|
|
81
|
+
if type(key) is not int:
|
|
82
|
+
return base.marker
|
|
83
|
+
try:
|
|
84
|
+
node = node[key]
|
|
85
|
+
except IndexError:
|
|
86
|
+
return base.marker
|
|
87
|
+
continue
|
|
88
|
+
# dict subclasses, custom containers, None, etc: full traversal
|
|
89
|
+
return SIMPLE_BAIL
|
|
90
|
+
return node
|
|
91
|
+
|
|
92
|
+
|
|
51
93
|
def iter_until_cut(gen):
|
|
52
94
|
"""
|
|
53
95
|
Consume a get generator until base.CUT_SENTINEL; yield values, stop on sentinel.
|
|
@@ -10,6 +10,7 @@ import pyparsing as pp
|
|
|
10
10
|
|
|
11
11
|
from . import base
|
|
12
12
|
from .base import MatchOp
|
|
13
|
+
from .utils import lazyprop
|
|
13
14
|
from .utypes import ANY
|
|
14
15
|
|
|
15
16
|
|
|
@@ -19,8 +20,11 @@ _MISSING = object()
|
|
|
19
20
|
class Const(MatchOp):
|
|
20
21
|
_match_from = ('Const',)
|
|
21
22
|
|
|
22
|
-
@
|
|
23
|
+
@lazyprop
|
|
23
24
|
def value(self):
|
|
25
|
+
"""
|
|
26
|
+
The literal value; computed once and cached on the instance.
|
|
27
|
+
"""
|
|
24
28
|
return self.args[0]
|
|
25
29
|
def matches(self, vals):
|
|
26
30
|
return (v for v in vals if self.value == v)
|
|
@@ -32,8 +36,11 @@ class Numeric(Const):
|
|
|
32
36
|
return str(self.args[0]) == str(int(self.args[0]))
|
|
33
37
|
except (ValueError, TypeError):
|
|
34
38
|
return False
|
|
35
|
-
@
|
|
39
|
+
@lazyprop
|
|
36
40
|
def value(self):
|
|
41
|
+
"""
|
|
42
|
+
The numeric value (int when possible); computed once and cached.
|
|
43
|
+
"""
|
|
37
44
|
return int(self.args[0]) if self.is_int() else float(self.args[0])
|
|
38
45
|
def __repr__(self):
|
|
39
46
|
return f'{self.value}'
|
|
@@ -5,7 +5,9 @@ import itertools
|
|
|
5
5
|
|
|
6
6
|
from . import predicates
|
|
7
7
|
from . import utils
|
|
8
|
-
from .access import Invert
|
|
8
|
+
from .access import Attr, Invert, Key, Slot
|
|
9
|
+
from .matchers import Const
|
|
10
|
+
from .utils import lazyprop
|
|
9
11
|
|
|
10
12
|
|
|
11
13
|
class rdoc(str):
|
|
@@ -35,6 +37,7 @@ class Dotted:
|
|
|
35
37
|
else:
|
|
36
38
|
self.guard = None
|
|
37
39
|
self.guard_op = predicates.EQ
|
|
40
|
+
self._hash = None
|
|
38
41
|
|
|
39
42
|
@property
|
|
40
43
|
def guard_negate(self):
|
|
@@ -72,10 +75,12 @@ class Dotted:
|
|
|
72
75
|
return tuple(sorted((k, Dotted._hashable(v)) for k, v in iterable))
|
|
73
76
|
return obj
|
|
74
77
|
def __hash__(self):
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
78
|
+
if self._hash is None:
|
|
79
|
+
try:
|
|
80
|
+
self._hash = hash((self.ops, self.transforms, self.guard, self.guard_op))
|
|
81
|
+
except TypeError:
|
|
82
|
+
self._hash = hash((self.ops, Dotted._hashable(self.transforms), self.guard, self.guard_op))
|
|
83
|
+
return self._hash
|
|
79
84
|
def __len__(self):
|
|
80
85
|
return len(self.ops)
|
|
81
86
|
def __iter__(self):
|
|
@@ -105,6 +110,28 @@ class Dotted:
|
|
|
105
110
|
def apply(self, val):
|
|
106
111
|
return apply_transforms(val, self.transforms)
|
|
107
112
|
|
|
113
|
+
@lazyprop
|
|
114
|
+
def simple_chain(self):
|
|
115
|
+
"""
|
|
116
|
+
Tuple of (kind, key) pairs when this path is simple — a plain chain
|
|
117
|
+
of concrete Key/Attr/Slot accesses with no patterns, substitutions,
|
|
118
|
+
references, guards, transforms, or filters — else None. Computed
|
|
119
|
+
once and cached; drives the fast path in get() that skips walk().
|
|
120
|
+
kind is 'key', 'attr', or 'slot'.
|
|
121
|
+
"""
|
|
122
|
+
if self.transforms or self.guard is not None:
|
|
123
|
+
return None
|
|
124
|
+
chain = []
|
|
125
|
+
for op in self.ops:
|
|
126
|
+
# exact types: subclasses like SlotSpecial have different semantics
|
|
127
|
+
if type(op) not in (Key, Attr, Slot):
|
|
128
|
+
return None
|
|
129
|
+
if not isinstance(op.op, Const):
|
|
130
|
+
return None
|
|
131
|
+
kind = 'attr' if isinstance(op, Attr) else 'slot' if isinstance(op, Slot) else 'key'
|
|
132
|
+
chain.append((kind, op.op.value))
|
|
133
|
+
return tuple(chain)
|
|
134
|
+
|
|
108
135
|
Dotted.registry.__doc__ = rdoc()
|
|
109
136
|
|
|
110
137
|
|
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
Shared type-checking helpers (duck-typing).
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
class lazyprop:
|
|
6
|
+
"""
|
|
7
|
+
Non-data descriptor: compute once on first access, cache the result
|
|
8
|
+
in the instance __dict__ (which then shadows the descriptor). Like
|
|
9
|
+
functools.cached_property but lock-free and available on 3.6+.
|
|
10
|
+
"""
|
|
11
|
+
def __init__(self, fn):
|
|
12
|
+
self.fn = fn
|
|
13
|
+
self.name = fn.__name__
|
|
14
|
+
self.__doc__ = fn.__doc__
|
|
15
|
+
|
|
16
|
+
def __get__(self, obj, owner=None):
|
|
17
|
+
if obj is None:
|
|
18
|
+
return self
|
|
19
|
+
val = self.fn(obj)
|
|
20
|
+
obj.__dict__[self.name] = val
|
|
21
|
+
return val
|
|
22
|
+
|
|
23
|
+
|
|
5
24
|
try:
|
|
6
25
|
import dataclasses as _dc
|
|
7
26
|
except ImportError:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dotted_notation
|
|
3
|
-
Version: 0.44.
|
|
3
|
+
Version: 0.44.4
|
|
4
4
|
Summary: Dotted notation for safe nested data traversal with optional chaining, pattern matching, and transforms
|
|
5
5
|
Author-email: Frey Waid <logophage1@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -176,6 +176,7 @@ Or pick only what you need:
|
|
|
176
176
|
- [Projection](#projection)
|
|
177
177
|
- [Unpack](#unpack)
|
|
178
178
|
- [Pack](#pack)
|
|
179
|
+
- [Recipes](#recipes)
|
|
179
180
|
- [FAQ](#faq)
|
|
180
181
|
- [Why do I get a tuple for my get?](#why-do-i-get-a-tuple-for-my-get)
|
|
181
182
|
- [How do I craft an efficient path?](#how-do-i-craft-an-efficient-path)
|
|
@@ -801,10 +802,13 @@ normal form. All three call `unpack()` internally.
|
|
|
801
802
|
>>> dotted.keys({'a': 1, 'b': 2}) & dotted.keys({'b': 3, 'c': 4})
|
|
802
803
|
{'b'}
|
|
803
804
|
|
|
804
|
-
All three accept `attrs=`
|
|
805
|
+
All three accept the same `attrs=`, `project=`, and `partial=` arguments as
|
|
806
|
+
`unpack`:
|
|
805
807
|
|
|
806
808
|
>>> dotted.keys({'point': Pt(3, 4)}, attrs=[dotted.Attrs.standard])
|
|
807
809
|
dict_keys(['point@x', 'point@y'])
|
|
810
|
+
>>> dotted.items({'a': {'b': 1, 'c': 2}, 'x': 3}, project='a')
|
|
811
|
+
dict_items([('a.b', 1), ('a.c', 2)])
|
|
808
812
|
|
|
809
813
|
<a id="build"></a>
|
|
810
814
|
### Build
|
|
@@ -2956,7 +2960,7 @@ Fragments concatenate naturally via `+` / `__radd__`; metadata merges:
|
|
|
2956
2960
|
>>> r = dotted.sqlize("age >= $(min_age)", driver='asyncpg')
|
|
2957
2961
|
>>> combined = "WHERE " + r.where
|
|
2958
2962
|
>>> r.build(combined, min_age=30)
|
|
2959
|
-
('WHERE age >= $1
|
|
2963
|
+
('WHERE age >= $1', [30])
|
|
2960
2964
|
|
|
2961
2965
|
### Hoisted params
|
|
2962
2966
|
|
|
@@ -3120,7 +3124,7 @@ Pass a shared `ParamPool` to every `sqlize()` call that should compose:
|
|
|
3120
3124
|
>>> r2 = dotted.sqlize('age >= 30', driver='asyncpg', pool=pool)
|
|
3121
3125
|
>>> combined = '(' + r1.where + ') AND (' + r2.where + ')'
|
|
3122
3126
|
>>> dotted.Resolver.build(combined, paramstyle='dollar-numeric')
|
|
3123
|
-
('(status = $1) AND (age
|
|
3127
|
+
('(status = $1) AND (age >= $2)', ['active', 30])
|
|
3124
3128
|
|
|
3125
3129
|
Substitutions by the same original name dedup across Resolvers sharing
|
|
3126
3130
|
a pool — one slot, one value, back-referenced in the rendered SQL:
|
|
@@ -3177,7 +3181,7 @@ segment before building a `Raw`:
|
|
|
3177
3181
|
Col('matched.customer')
|
|
3178
3182
|
>>> Col('schema', 'table', 'col')
|
|
3179
3183
|
Col('schema.table.col')
|
|
3180
|
-
>>> Col('bad; DROP TABLE')
|
|
3184
|
+
>>> Col('bad; DROP TABLE') # doctest: +IGNORE_EXCEPTION_DETAIL
|
|
3181
3185
|
Traceback (most recent call last):
|
|
3182
3186
|
...
|
|
3183
3187
|
dotted.TranslationError: Col part is not a plain identifier: 'bad; DROP TABLE'
|
|
@@ -3423,6 +3427,79 @@ example, removing an entire group without listing every key:
|
|
|
3423
3427
|
echo '{"db.host": "localhost", "db.port": 5432, "app.debug": true}' | dq --pack --unpack remove -p db
|
|
3424
3428
|
# {"app.debug": true}
|
|
3425
3429
|
|
|
3430
|
+
<a id="recipes"></a>
|
|
3431
|
+
## Recipes
|
|
3432
|
+
|
|
3433
|
+
A grab-bag of one-liners that show off what the notation can do. Every example
|
|
3434
|
+
below runs as-is.
|
|
3435
|
+
|
|
3436
|
+
**Flatten a nested list** (leaves only, any depth) — recurse through every slot
|
|
3437
|
+
with `*([*])`, then keep only the deepest match on each branch with `:-1`:
|
|
3438
|
+
|
|
3439
|
+
>>> import dotted
|
|
3440
|
+
>>> dotted.get([1, 2, 3, [4, 5, [6, 7]]], '*([*]):-1')
|
|
3441
|
+
(1, 2, 3, 4, 5, 6, 7)
|
|
3442
|
+
|
|
3443
|
+
Because `:-1` is deepest-*per-branch*, shallow leaves survive alongside deep
|
|
3444
|
+
ones — a ragged list still flattens completely.
|
|
3445
|
+
|
|
3446
|
+
**Collect every leaf value** of a nested dict — same idea with key recursion:
|
|
3447
|
+
|
|
3448
|
+
>>> dotted.get({'a': {'b': 1}, 'c': 2}, '**:-1')
|
|
3449
|
+
(1, 2)
|
|
3450
|
+
|
|
3451
|
+
**Find a key at any depth** — `**` recurses through dict keys, then continue
|
|
3452
|
+
with the key you want:
|
|
3453
|
+
|
|
3454
|
+
>>> dotted.get({'a': {'b': {'name': 'x'}}, 'name': 'y'}, '**.name')
|
|
3455
|
+
('x',)
|
|
3456
|
+
|
|
3457
|
+
If the tree mixes lists and dicts, recurse through both with `*(*#, [*])`:
|
|
3458
|
+
|
|
3459
|
+
>>> dotted.get({'kids': [{'name': 'a'}, {'name': 'b'}]}, '*(*#, [*]).name')
|
|
3460
|
+
('a', 'b')
|
|
3461
|
+
|
|
3462
|
+
**Find values matching a condition anywhere** — attach a value guard to the
|
|
3463
|
+
recursive walk:
|
|
3464
|
+
|
|
3465
|
+
>>> dotted.get({'a': {'b': 7, 'c': 3}, 'd': {'e': 9}}, '**>5')
|
|
3466
|
+
(7, 9)
|
|
3467
|
+
|
|
3468
|
+
**Bulk-update everything that matches** — the same pattern drives `update` and
|
|
3469
|
+
`remove`:
|
|
3470
|
+
|
|
3471
|
+
>>> dotted.update({'a': {'b': 7, 'c': 3}, 'd': 7}, '**=7', 99)
|
|
3472
|
+
{'a': {'b': 99, 'c': 3}, 'd': 99}
|
|
3473
|
+
|
|
3474
|
+
**Filter a list of dicts, then project a field** — combine a key-value filter
|
|
3475
|
+
with a continuation:
|
|
3476
|
+
|
|
3477
|
+
>>> users = [{'name': 'x', 'active': True},
|
|
3478
|
+
... {'name': 'y', 'active': False},
|
|
3479
|
+
... {'name': 'z', 'active': True}]
|
|
3480
|
+
>>> dotted.get(users, '[*&active=true].name')
|
|
3481
|
+
('x', 'z')
|
|
3482
|
+
|
|
3483
|
+
**Upsert** — update a list entry if it exists, else append, using cut (`#`) in a
|
|
3484
|
+
disjunction so the first matching branch wins:
|
|
3485
|
+
|
|
3486
|
+
>>> dotted.update({'emails': [{'email': 'a@x'}]},
|
|
3487
|
+
... 'emails[(*&email="a@x"#, +)].email', 'NEW')
|
|
3488
|
+
{'emails': [{'email': 'NEW'}]}
|
|
3489
|
+
>>> dotted.update({'emails': [{'email': 'a@x'}]},
|
|
3490
|
+
... 'emails[(*&email="z@x"#, +)]', {'email': 'z@x'})
|
|
3491
|
+
{'emails': [{'email': 'a@x'}, {'email': 'z@x'}]}
|
|
3492
|
+
|
|
3493
|
+
**Coerce on the way out** — pipe a value through transforms:
|
|
3494
|
+
|
|
3495
|
+
>>> dotted.get({'name': 'bob', 'tags': [1, 2, 3]}, 'name|uppercase')
|
|
3496
|
+
'BOB'
|
|
3497
|
+
>>> dotted.get({'name': 'bob', 'tags': [1, 2, 3]}, 'tags|len')
|
|
3498
|
+
3
|
|
3499
|
+
|
|
3500
|
+
See [Recursive Traversal](#recursive-traversal), [Filters](#filters), and
|
|
3501
|
+
[Transforms](#transforms) for the full story behind each of these.
|
|
3502
|
+
|
|
3426
3503
|
<a id="faq"></a>
|
|
3427
3504
|
## FAQ
|
|
3428
3505
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "dotted_notation"
|
|
7
|
-
version = "0.44.
|
|
7
|
+
version = "0.44.4"
|
|
8
8
|
description = "Dotted notation for safe nested data traversal with optional chaining, pattern matching, and transforms"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.6"
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dotted_notation-0.44.2 → dotted_notation-0.44.4}/dotted_notation.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|