Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Typing Improvements #67

Merged
merged 29 commits into from
Feb 17, 2024
Merged

Typing Improvements #67

merged 29 commits into from
Feb 17, 2024

Conversation

TomHall2020
Copy link
Contributor

Closes #61

Should be compatible with python 3.9+ syntax. In order of increasing intensity:

  • Replaced Dict, Tuple, List etc.
  • Use type variable in handicap equations to show when floats or arrays are given as input and are returned as output
  • type hints for fixed dictionaries for AGB classification data that are read from JSON
  • enforce correct scoring systems for target class using literal types.

The sum of the last three points allow for removing some of the extra assertions that were in place just to please mypy, especially in the score_for_round functions.

There are still a couple of Anys floating around that I might be able to excise, but this feels like most of it, and enough that I could do with a check over to see if anything else needs included in this one.

On the topic of making the scoring systems explicitly typed, I did think about looking at handicap systems as well, either as a literal type or an Enum, but with potential refactors coming from #2 that seemed like a step too far.

Other general feedback from the process of working on this:

  • Some of the JSON files being read in are extremely small and I question the value of having to read them in every time they are used vs just defining them in code, in particular the AGB Genders (which the functions to build the classification dictionaries assume the contents of anyway).
  • Type hints in the test files were a bit of a pain in the backside and a few needed to be changed/ignored, which adds a bit more friction to development.

In process of fixing type hints in constructor function had to refactor slightly, less repeated dictionary assignment and instead just create a dictionary with required keys and values in the final step before assigning to the groupname
Plus refactors to dictionary constructor function
Implementation here could be made neater by building the output dict from a fixed input dict
…n functions, remove OrderedDict

Dictionary ordering guranteed in python 3.7+ anyway so not required. Type hinted group data dictionaries obliviate need to manually assign type of class_data
floatarray type enough to show that a single handicap in gives single handicap values back out
Use literal type to constrain allowable parameter values for any funciton constructing a Target instance
…isting

Use typing.get_args to deduplicate list of allowable scoring systems
@TomHall2020
Copy link
Contributor Author

Apologies, didn't realise my main branch was behind, merge conflicts fixed on my end but worth a double check.

Copy link
Owner

@jatkinson1000 jatkinson1000 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @TomHall2020 this is great!
A couple of queries/comments and lines I think may now be redundant.

Apologies for not telling you about the docs - I had it on my mind to drop you a message saying you'd want to rebase, but forgot. -_- PS - I wrote some docs!

If you could look at the comments, amend and push with formatting and linting that'd be great. On a side note, how would you feel about using pre-commit on the repo. In general I'm not that keen on it, but I can see it could be useful on this project?

General queries:

Changes from Union[int, float] to float are because python is 'clever enough' to take an integer input and just convert it to a float, so as long as we always use floats internally mypy doesn't complain? I think I'm still thinking in Fortran/C after a week of work.

I wonder if the ScoringSystem type should live in utils or target.
I think I err on it being in target as you have done given it is directly associated with targets.

@jatkinson1000
Copy link
Owner

Oh, and python 3.9 doesn't have TypeAlias, but I am erring on dropping and making the project 3.10+.
This resolves the issue over in #59 and given the current users I don't think is a real issue.
I'd want to do this in a separate PR that you can rebase this on rather than here, though.

@jatkinson1000
Copy link
Owner

Regarding the JSON, yes, it had crossed my mind that reading every time isn't ideal, but it's not a bottleneck.
Some things could be defined in the code, but these files (specifically the class_func.read_X_json() functions) are also used to set up a database for archerycalculator and having a single point of reference is nice.
I can see that the information might be moved to constants.py in a different PR but I like to have it defined clearly and concisely in one place rather than being embedded throughout functions/files.
I'll have a ponder.

@jatkinson1000 jatkinson1000 added documentation Improvements or additions to documentation enhancement New feature or request labels Feb 15, 2024
@TomHall2020
Copy link
Contributor Author

Oh, and python 3.9 doesn't have TypeAlias, but I am erring on dropping and making the project 3.10+. This resolves the issue over in #59 and given the current users I don't think is a real issue. I'd want to do this in a separate PR that you can rebase this on rather than here, though.

Good spot. It's not necessary, just more explicit. We could install typing_extensions and get access to it, as well as other goodies like Self (3.11), though theres not that much else I've found myself looking for. Otherwise can just remove and add back in after a bump to 3.10.

@TomHall2020
Copy link
Contributor Author

On a side note, how would you feel about using pre-commit on the repo. In general I'm not that keen on it, but I can see it could be useful on this project?

I've never had to use it before but aware of it; I can't really offer any experience for an opinion, but I guess the question is the benifit of keeping every commit clean worth forcing all future contributors be comfortable/able to use it before they can add anything to the project?

Changes from Union[int, float] to float are because python is 'clever enough' to take an integer input and just convert it to a float, so as long as we always use floats internally mypy doesn't complain? I think I'm still thinking in Fortran/C after a week of work.

Less that it converts it to a float but just that ints are compatible with operations on floats. mypy docs explain it, essentially so long as you don't explicitly call any float only methods you are generally safe to assume ints are usable anywhere floats are.

I wonder if the ScoringSystem type should live in utils or target. I think I err on it being in target as you have done given it is directly associated with targets.

Agree here, makes sense to keep it close to where its used, and importable from the same place as Target (any consumer wanting to write a type checked function that creates Targets would need the ScoringSystem type too).

@TomHall2020
Copy link
Contributor Author

Regarding the JSON, yes, it had crossed my mind that reading every time isn't ideal, but it's not a bottleneck. Some things could be defined in the code, but these files (specifically the class_func.read_X_json() functions) are also used to set up a database for archerycalculator and having a single point of reference is nice. I can see that the information might be moved to constants.py in a different PR but I like to have it defined clearly and concisely in one place rather than being embedded throughout functions/files. I'll have a ponder.

Makes sense, I didn't consider those files being read in by other projects.

Out of scope for this but perhaps a good compromise is to just call the read_json functions once in a module (either classification_utils or a seperate classifcation_constants), assign the results to constants and then import those in each of the modules they are used.

Actually has the side benifit of moving those to global variables rather than locals in the _make_classification_dict functions.

Older version did not correct module docstring spacing
@TomHall2020
Copy link
Contributor Author

Thats annoying, I did run black to generate the final commit there, but it didn't fix the spacing of module docstrings to imports on my local version

❯ black archeryutils
reformatted /Users/Tom/dev/forks/archeryutils/archeryutils/classifications/agb_field_classifications.py
reformatted /Users/Tom/dev/forks/archeryutils/archeryutils/classifications/agb_outdoor_classifications.py
reformatted /Users/Tom/dev/forks/archeryutils/archeryutils/rounds.py
reformatted /Users/Tom/dev/forks/archeryutils/archeryutils/targets.py
reformatted /Users/Tom/dev/forks/archeryutils/archeryutils/handicaps/handicap_equations.py
reformatted /Users/Tom/dev/forks/archeryutils/archeryutils/tests/test_rounds.py
reformatted /Users/Tom/dev/forks/archeryutils/archeryutils/tests/test_targets.py
reformatted /Users/Tom/dev/forks/archeryutils/archeryutils/handicaps/tests/test_handicaps.py

All done! ✨ 🍰 ✨
8 files reformatted, 21 files left unchanged.

❯ which black
/Users/Tom/dev/forks/archeryutils/.venv/bin/black

❯ black --version
black, 23.12.1 (compiled: yes)
Python (CPython) 3.11.5

And now I see that the version of black in the project requirements got bumped and I hadn't reinstalled the dependencies, so that explains it.

@jatkinson1000
Copy link
Owner

Out of scope for this but perhaps a good compromise is to just call the read_json functions once in a module (either classification_utils or a separate classification_constants), assign the results to constants and then import those in each of the modules they are used.

Actually has the side benefit of moving those to global variables rather than locals in the _make_classification_dict functions.

Please open an issue (no obligation to take it on, but I agree this would be better so should document it.)
It would be an internal change I think, so not required for release.

Copy link
Owner

@jatkinson1000 jatkinson1000 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I think this is pretty much good to go.

One thing would be if you could take a look over the docs and examples notebook to see if you feel anything needs updating as a result of these changes.
(It may well be that the answer is no as much of this is internals).
Possible things from a quick glance are:

  • Target docstring - does it need the type updating to Literal/ScoringSystem for system?
  • Rounds can now take iterable of passes, could have an example in the docstring for this, at least the tuple?

I added some general guidance on PRs here that includes this, but this work predates that, so don't worry about format for this.

My last query is about the apparent deprecation of TypeAlias in 3.12 (see comment) and whether we want to leave the code as is here (and remove the comment about bumping in v3.10+)?

@TomHall2020
Copy link
Contributor Author

TomHall2020 commented Feb 17, 2024

My last query is about the apparent deprecation of TypeAlias in 3.12 (see comment) and whether we want to leave the code as is here (and remove the comment about bumping in v3.10+)?

Replied to the comment: in short I think it doesn't hurt to leave it since its unlikely to be there for long, and helps with discoverability.

Will take a look through the docs, I'm not sure what best practice is for an aliased literal type like we have for scoring system. I've tried to find an example somewhere as to whether documentation mentions the actual base type for these, the aliased type or both.
The numpy docstring style guide suggests

When a parameter can only assume one of a fixed set of values, those values can be listed in braces, with the default appearing first:

     scoring_system : {
        "5_zone", "10_zone", "10_zone_compound", "10_zone_6_ring", 
        "10_zone_5_ring", "10_zone_5_ring_compound", "WA_field", "IFAA_field", 
        "IFAA_field_expert", "Beiter_hit_miss", "Worcester", "Worcester_2_ring"
    }
        target face/scoring system type.

This would be pretty long but would have the advantage of enumerating all the supported scoring systems right where a user might need to see them (in the docs for the Target class), disadvantage is it needs to be kept up to date with the code as new ones are added but that shouldn't be too often.

However the style guide doesn't cover situations where a TypeAlias is used for a complex or lengthy (this case) type.

Other options to keep it shorter include:

    scoring_system : ScoringSystem  # just referencing the alias:
    scoring_system : targets.ScoringSystem  # qualifying the alias with module name
    scoring_system : str (ScoringSystem)  # trying to be clear that its a string
    scoring_system : ScoringSystem[str] # cursed syntax, honestly don't know if this helps more than it confuses...
        target face/scoring system type.

Given the type is repeated in the docstring as both an init parameter and an attribute, I would lean towards choosing the explict list for the Parameters section (the target will literally refuse to initalise if you don't provide one of these strings), and then just describing the aliased and/or base type in the Attributes section (you can change it afterwards if you want, but mypy will shout at you)

@TomHall2020
Copy link
Contributor Author

TomHall2020 commented Feb 17, 2024

Figuting out how to get sphinx to play nicely with displaying the allowed values is defeating me here, so I'm not going to get too drawn into it. I've found from playing that if I put an empty line between the brackets don't provide the type in the docstring sphinx is autofilling the allowed literal values when it builds the docs:

    scoring_system : 
        target face/scoring system type. Must be one of the supported values.

Gives this:

Screenshot 2024-02-16 at 17 22 44

But clearly this is not much use in the actual docstring.

I've left it as something that isn't quite as nicely formatted in the html docs, but fits with the suggested numpy docstring syntax, doesn't autofill and duplicate its values, and shows the relevant information in the docstring itself.

    scoring_system : {\
        "5_zone", "10_zone", "10_zone_compound", "10_zone_6_ring",\
        "10_zone_5_ring", "10_zone_5_ring_compound", "WA_field", "IFAA_field",\
        "IFAA_field_expert", "Beiter_hit_miss", "Worcester", "Worcester_2_ring"}
        target face/scoring system type. Must be one of the supported values.

Copy link

codecov bot commented Feb 17, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Comparison is base (b397939) 95.45% compared to head (ca8f83a) 95.81%.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main      #67      +/-   ##
==========================================
+ Coverage   95.45%   95.81%   +0.35%     
==========================================
  Files          24       24              
  Lines        1321     1339      +18     
==========================================
+ Hits         1261     1283      +22     
+ Misses         60       56       -4     
Files Coverage Δ
...utils/classifications/agb_field_classifications.py 100.00% <100.00%> (+1.49%) ⬆️
...tils/classifications/agb_indoor_classifications.py 100.00% <100.00%> (+1.56%) ⬆️
.../classifications/agb_old_indoor_classifications.py 100.00% <100.00%> (+2.08%) ⬆️
...ils/classifications/agb_outdoor_classifications.py 100.00% <100.00%> (+0.84%) ⬆️
...cheryutils/classifications/classification_utils.py 93.05% <100.00%> (+2.67%) ⬆️
...cheryutils/classifications/tests/test_agb_field.py 100.00% <ø> (ø)
...heryutils/classifications/tests/test_agb_indoor.py 100.00% <ø> (ø)
...utils/classifications/tests/test_agb_old_indoor.py 100.00% <ø> (ø)
...eryutils/classifications/tests/test_agb_outdoor.py 100.00% <ø> (ø)
archeryutils/handicaps/handicap_equations.py 100.00% <100.00%> (ø)
... and 7 more

Copy link
Owner

@jatkinson1000 jatkinson1000 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good to go, thanks so much.

Added a couple of nits, docstring for Pass, and made ScoringSystem docstring look nicer with rest systax.

Will merge shortly (just considering best merge format) 🚀

@jatkinson1000 jatkinson1000 merged commit 649259c into jatkinson1000:main Feb 17, 2024
12 checks passed
@jatkinson1000 jatkinson1000 mentioned this pull request Feb 17, 2024
@TomHall2020 TomHall2020 deleted the typing branch February 19, 2024 12:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Type hinting improvements
2 participants