mahmoud / glom

☄️ Python's nested data operator (and CLI), for all your declarative restructuring needs. Got data? Glom it! ☄️
https://glom.readthedocs.io
Other
1.89k stars 61 forks source link

Arg Mode #196

Closed kurtbrose closed 2 years ago

kurtbrose commented 4 years ago

Make T()-args, Invoke()-args, Assign(), and several default-args all have consistent behavior. Prior to this, different spec types would treat their argument as a constant, and others would treat it as a spec; often there were special case cut-outs for T (and only T).

kurtbrose commented 4 years ago

I think this is clearly on the right track -- Call() was already basically doing arg-mode, but doing it with a bunch of manual tap-dancing

image

mahmoud commented 4 years ago

Nice, yeah, there are a variety of precedents set in the existing specs. Invoke doesn't do that, though, does it? We ended up going with a more explicit approach because of the discomfort with Call's handling of this?

And for anyone following along (as well as for my own recollection), here's the bright line:

Should "subspec"-style arguments to specifier types respect the mode of the surrounding spec? Or should they be consistent across all specs and contexts?

This probably doesn't need to be democratized, and can be determined by comparing before-and-after with some existing cases, hence this PR.

kurtbrose commented 4 years ago

To unpack it a bit more -- in general subspecs should clearly follow Mode; the proposal here isn't to change that.

However, currently there are three kinds of values that give you different behavior:

Depending on what you are doing "clearly" one or the other is the correct behavior. There's a lot of work trying to thread that needle:

So, this is the latest in what has been a continuous attempt to sand down a rough edge.

What is the scope of applicability? These are some guidelines:

These three criteria often overlap -- as was the case with Call().

As a simple motivating example:

S(a=T)  # stores current Target in S.a
S(a=(T, 1))  # evals T, tries to pass to 1 then fails
S(a=1)  # error: 1 is not a valid spec

with arg-mode

S(a=T)  # stores Target in S.a
S(a=(T, 1)) # stores (Target, 1) in S.a
S(a=1)  # stores 1 in S.a
kurtbrose commented 4 years ago

What are the downsides?

The goal is making fewer special-case cutouts for T by having this new mode, and ending up with an overall simpler, more consistent, more comprehensible, more powerful system.

There's also some interesting interactions with M. We pulled M(subspec) and restricted it to only T. So, this is another special case of T. I don't think it is a good use case for arg-mode, because M() checks for truthiness -- which for all the data structures handled by arg-more will mean "is it empty", which will either be always true or always false.

kurtbrose commented 4 years ago

some more funny cases of arg-mode vs current behavior

S(a=[])  # in arg-mode, stores a new empty list each time; currently, errors
S(a=Val([]))  # stores the same empty list every time -- unlike arg-mode has same problem as python defaults
S(a=[T])  # in arg mode, stores [Target]; in Auto mode, if Target is iterable, stores list(Target)
S(a=list)  # in arg mode, stores `list` type; in Auto, list(Target)
kurtbrose commented 4 years ago

So, tentatively I think I've found the places that can benefit:

On balance, I think the changes to the API make things simpler.

The biggest potential source of confusion is str-as-constant vs str-as-spec in the case of S() -- since this feature has only been out for ~ 6 weeks, I think maybe we can get out ahead of it and change the behavior without ruffling too many feathers.

kurtbrose commented 4 years ago

definitely the changes that were required to unit tests are interesting

kurtbrose commented 4 years ago

oops I missed Assign() -- the val argument there would probably benefit from arg-mode

THAT API has been around for a long time, so changing the behavior of Assign(Spec(...)) is probably a bad idea; will need to think about how to allow arg_val(Spec()) to NOT change the Mode for child specs; Group has a similar challenge where ideally the Mode would project down through constants, but when hitting a Spec class would revert to the outer enclosing Mode; I didn't pursue it further with Group b/c it seems to be a rare use case -- with it popping up where maybe I can think of a generic solution

inside Assign.glomit():

        if type(self.val) is Spec:
            val = scope[glom](target, self.val, scope)
        else:
            val = self.val

that special casing is what we can get rid of with arg-mode :-)

kurtbrose commented 4 years ago

auto mode dict keys could also benefit (another place of T special casing)

kurtbrose commented 3 years ago

found kind of an interesting corner case with trace rendering; locally all tests are green now

I think it is ready to go

kurtbrose commented 3 years ago

maybe I should add stuff to the docs though?

codecov[bot] commented 2 years ago

Codecov Report

Merging #196 (0c7c97e) into master (032f252) will increase coverage by 0.06%. The diff coverage is 100.00%.

@@            Coverage Diff             @@
##           master     #196      +/-   ##
==========================================
+ Coverage   98.38%   98.44%   +0.06%     
==========================================
  Files          27       27              
  Lines        4338     4509     +171     
  Branches      710      784      +74     
==========================================
+ Hits         4268     4439     +171     
  Misses         46       46              
  Partials       24       24              
Impacted Files Coverage Δ
glom/test/test_basic.py 100.00% <ø> (ø)
glom/test/test_error.py 99.53% <ø> (-0.02%) :arrow_down:
glom/core.py 98.68% <100.00%> (+0.16%) :arrow_up:
glom/matching.py 100.00% <100.00%> (ø)
glom/mutation.py 99.33% <100.00%> (-0.01%) :arrow_down:
glom/test/test_match.py 100.00% <100.00%> (ø)
glom/test/test_mutation.py 98.11% <100.00%> (+0.06%) :arrow_up:
glom/test/test_path_and_t.py 99.06% <100.00%> (+0.04%) :arrow_up:
glom/test/test_scope_vars.py 100.00% <100.00%> (ø)

:mega: Codecov can now indicate which changes are the most critical in Pull Requests. Learn more