matplotlib-map-utils 2.0.2__py3-none-any.whl → 3.0.0__py3-none-any.whl

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.
@@ -0,0 +1,336 @@
1
+ import re
2
+ import json
3
+ import warnings
4
+ from importlib import resources
5
+ from typing import List, Literal, Union
6
+
7
+ # Literal lists, for intellisense
8
+ regions = Literal["Midwest", "Northeast", "South", "West",
9
+ "Inhabited Territory", "Uninhabited Territory", "Sovereign State"]
10
+
11
+ divisions = Literal["East North Central", "East South Central", "Mid-Atlantic", "Mountain",
12
+ "New England", "Pacific", "South Atlantic", "West North Central", "West South Central",
13
+ "Commonwealth", "Compact of Free Association", "Incorporated and Unorganized",
14
+ "Unincorporated and Unorganized", "Unincorporated and Organized"]
15
+
16
+ ombs = Literal["Region I", "Region II", "Region III", "Region IV", "Region IX", "Region V",
17
+ "Region VI", "Region VII", "Region VIII", "Region X",
18
+ "Inhabited Territory", "Uninhabited Territory", "Sovereign State"]
19
+
20
+ beas = Literal["Far West", "Great Lakes", "Mideast", "New England", "Plains",
21
+ "Rocky Mountain", "Southeast", "Southwest",
22
+ "Inhabited Territory", "Uninhabited Territory", "Sovereign State"]
23
+
24
+ returns = Literal["fips","name","abbr","object","dict"]
25
+
26
+ class USA:
27
+ # No arguments need to pass on initialization really
28
+ def __init__(self):
29
+ self._jurisdictions = self._load_json()
30
+
31
+ # This is just for loading the JSON
32
+ def _load_json(self):
33
+ with resources.files("matplotlib_map_utils.utils").joinpath("usa.json").open("r") as f:
34
+ usa_json = json.load(f)
35
+ return usa_json
36
+
37
+ # Getter for all jurisdictions, VALID OR NOT
38
+ @property
39
+ def _all(self):
40
+ return self._jurisdictions
41
+
42
+ # Getter for all valid jurisdictions
43
+ @property
44
+ def jurisdictions(self):
45
+ return self.filter_valid(True, self._all, "object")
46
+
47
+ # Getter for all valid states
48
+ @property
49
+ def states(self):
50
+ return self.filter_state(True, self.jurisdictions, "object")
51
+
52
+ # Getter for all valid territories
53
+ @property
54
+ def territories(self):
55
+ return self.filter_territory(True, self.jurisdictions, "object")
56
+
57
+ # Getters to generate distinct values for Region, Division, OMB, and BEA
58
+ # which are useful if you can't recall which options are valid
59
+ # First, the function that will get the distinct values
60
+ def _distinct_options(self, key):
61
+ # First getting all the available options from the list
62
+ options = [j[key] for j in self.jurisdictions if j[key] is not None]
63
+ # Creating the distinct set
64
+ options_set = set(options)
65
+ # Returning the set (but as a list)
66
+ # this will also be alphabetically sorted
67
+ options = list(options_set)
68
+ options.sort()
69
+ return options
70
+
71
+ # The getters are now just calls to the properties
72
+ @property
73
+ def regions(self):
74
+ return self._distinct_options("region")
75
+
76
+ @property
77
+ def divisions(self):
78
+ return self._distinct_options("division")
79
+
80
+ @property
81
+ def omb(self):
82
+ return self._distinct_options("omb")
83
+
84
+ @property
85
+ def bea(self):
86
+ return self._distinct_options("bea")
87
+
88
+ # Main filter function
89
+ # Each filter step will follow the same process
90
+ ## Check that there is a non-None filter
91
+ ## Normalize the input to be in a list (if not already)
92
+ ## Perform the filter step
93
+ # Each step is also available as its own independent function, as needed
94
+ def filter(self, valid: bool | None=True,
95
+ fips: str | int | None=None,
96
+ name: str | None=None,
97
+ abbr: str | None=None,
98
+ state: bool | None=None,
99
+ contiguous: bool | None=None,
100
+ territory: bool | None=None,
101
+ region: Union[regions, List[regions]]=None,
102
+ division: Union[divisions, List[divisions]]=None,
103
+ omb: Union[ombs, List[ombs]]=None,
104
+ bea: Union[beas, List[beas]]=None,
105
+ to_return: Union[returns, List[returns]]="fips"):
106
+
107
+ # Getting a copy of our jurisdictions, which will be filtered each time
108
+ filter_juris = self.jurisdictions.copy()
109
+
110
+ # Starting with an initial valid filtering
111
+ # Which will drop invalid FIPS codes 03, 07, 14, 43, and 52
112
+ if (valid is not None) and (len(filter_juris) > 0):
113
+ filter_juris = self.filter_valid(valid, filter_juris, to_return="_ignore")
114
+
115
+ # Going through each step
116
+ if (fips is not None) and (len(filter_juris) > 0):
117
+ filter_juris = self.filter_fips(fips, filter_juris, to_return="_ignore")
118
+
119
+ if (name is not None) and (len(filter_juris) > 0):
120
+ filter_juris = self.filter_name(name, filter_juris, to_return="_ignore")
121
+
122
+ if (abbr is not None) and (len(filter_juris) > 0):
123
+ filter_juris = self.filter_abbr(abbr, filter_juris, to_return="_ignore")
124
+
125
+ if (state is not None) and (len(filter_juris) > 0):
126
+ filter_juris = self.filter_state(state, filter_juris, to_return="_ignore")
127
+
128
+ if (contiguous is not None) and (len(filter_juris) > 0):
129
+ filter_juris = self.filter_contiguous(contiguous, filter_juris, to_return="_ignore")
130
+
131
+ if (territory is not None) and (len(filter_juris) > 0):
132
+ filter_juris = self.filter_territory(territory, filter_juris, to_return="_ignore")
133
+
134
+ if (region is not None) and (len(filter_juris) > 0):
135
+ filter_juris = self.filter_region(region, filter_juris, to_return="_ignore")
136
+
137
+ if (division is not None) and (len(filter_juris) > 0):
138
+ filter_juris = self.filter_division(division, filter_juris, to_return="_ignore")
139
+
140
+ if (omb is not None) and (len(filter_juris) > 0):
141
+ filter_juris = self.filter_omb(omb, filter_juris, to_return="_ignore")
142
+
143
+ if (bea is not None) and (len(filter_juris) > 0):
144
+ filter_juris = self.filter_bea(bea, filter_juris, to_return="_ignore")
145
+
146
+ # Final step is to process the input based on to_return
147
+ # and then return it!
148
+ return self._process_return(filter_juris, to_return)
149
+
150
+ # Filtering bool values (valid, state, contiguous, territory)
151
+ # Will accept either true or false
152
+ def _filter_bool(self, value, key, to_filter=None, to_return="_ignore"):
153
+ # If nothing is passed to to_filter, getting the jurisdictions list
154
+ to_filter = self.jurisdictions.copy() if to_filter is None else to_filter
155
+ if not isinstance(value, bool):
156
+ warnings.warn(f"Invalid {key} filter: {value}. Only boolean values (True/False) are considered valid, see documentation for details.")
157
+ else:
158
+ # Performing the filter
159
+ filtered = [j for j in to_filter if j[key] == value]
160
+ # And returning the values
161
+ return self._process_return(filtered, to_return)
162
+
163
+ # Shortcuts for filtering based on valid, state, contiguous, and territory
164
+ def filter_valid(self, valid: bool, to_filter=None, to_return="fips"):
165
+ return self._filter_bool(valid, "valid", to_filter, to_return)
166
+
167
+ def filter_state(self, state: bool, to_filter=None, to_return="fips"):
168
+ return self._filter_bool(state, "state", to_filter, to_return)
169
+
170
+ def filter_contiguous(self, contiguous: bool, to_filter=None, to_return="fips"):
171
+ return self._filter_bool(contiguous, "contiguous", to_filter, to_return)
172
+
173
+ def filter_territory(self, territory: bool, to_filter=None, to_return="fips"):
174
+ return self._filter_bool(territory, "territory", to_filter, to_return)
175
+
176
+ # Filtering FIPS
177
+ # Will accept an integer or a two-digit string as an input
178
+ # If a longer string is inserted, will truncate to only the first two characters
179
+ def filter_fips(self, fips: str | List[str], to_filter=None, to_return="abbr"):
180
+ # If nothing is passed to to_filter, getting the jurisdictions list
181
+ to_filter = self.jurisdictions.copy() if to_filter is None else to_filter
182
+ # Normalizing the fips value being passed
183
+ fips = self._normalize_input(fips)
184
+ # This will store the cleaned-up fips codes
185
+ fips_clean = []
186
+ for f in fips:
187
+ # If the input is an integer, convert it to a two-digit string
188
+ if isinstance(f, int):
189
+ fips_clean.append(str(f).zfill(2)[:2])
190
+ # If the input is already a string, get the first two characters
191
+ elif isinstance(f, str):
192
+ fips_clean.append(f.zfill(2)[:2])
193
+ # Otherwise, throw a *warning*
194
+ else:
195
+ warnings.warn(f"Invalid FIPS filter: {f}. Only integers and strings are considered valid, see documentation for details.")
196
+ # Now can use the clean fips to actually filter
197
+ filtered = [j for j in to_filter if j["fips"] in fips_clean]
198
+ # And returning the values
199
+ return self._process_return(filtered, to_return)
200
+
201
+ # Filtering name
202
+ # Will accept strings
203
+ # Will normalize the string first (trim, case, special characters), before checking
204
+ # Some states also have an alias available for checking against (Washington, D.C. and District of Columbia are equivalent)
205
+ def filter_name(self, name: str | List[str], to_filter=None, to_return="fips"):
206
+ # If nothing is passed to to_filter, getting the jurisdictions list
207
+ to_filter = self.jurisdictions.copy() if to_filter is None else to_filter
208
+ # Normalizing the name input being passed
209
+ name = self._normalize_input(name)
210
+ # This will store the cleaned-up name input
211
+ name_clean = []
212
+ for n in name:
213
+ # If the input is a string, clean it
214
+ if isinstance(n, str):
215
+ name_clean.append(self._normalize_string(n, case="lower"))
216
+ else:
217
+ warnings.warn(f"Invalid name filter: {n}. Only strings are considered valid, see documentation for details.")
218
+ # Now we can use the clean name to filter
219
+ # Note that we also normalize the names and aliases in our to_filter list!
220
+ filtered = [j for j in to_filter if ((self._normalize_string(j["name"], case="lower") in name_clean) or
221
+ (j["alias"] is not None and self._normalize_string(j["alias"], case="lower") in name_clean))]
222
+ # And returning the values
223
+ return self._process_return(filtered, to_return)
224
+
225
+ # Filtering abbr
226
+ # Will accept strings
227
+ # Will normalize the string first (trim, case, special characters), before checking
228
+ # If a string longer than two characters is passed, will only look at the first two characters!
229
+ def filter_abbr(self, abbr: str | List[str], to_filter=None, to_return="fips"):
230
+ # If nothing is passed to to_filter, getting the jurisdictions list
231
+ to_filter = self.jurisdictions.copy() if to_filter is None else to_filter
232
+ # Normalizing the input being passed
233
+ abbr = self._normalize_input(abbr)
234
+ # This will store the cleaned-up input
235
+ abbr_clean = []
236
+ for a in abbr:
237
+ # If the input is a string, clean it
238
+ if isinstance(a, str):
239
+ abbr_clean.append(self._normalize_string(a, case="lower"))
240
+ else:
241
+ warnings.warn(f"Invalid abbr filter: {a}. Only strings are considered valid, see documentation for details.")
242
+ # Now we can use the clean input to filter
243
+ filtered = [j for j in to_filter if (j["abbr"] is not None and self._normalize_string(j["abbr"], case="lower")[:2] in abbr_clean)]
244
+ # And returning the values
245
+ return self._process_return(filtered, to_return)
246
+
247
+ # Filtering for categorical values (region/division/omb/bea)
248
+ # Will get the list of acceptable values and compare inputs to it
249
+ # while also warning if an invalid filter is requested
250
+ def _filter_categorical(self, input, key, to_filter=None, to_return="_ignore"):
251
+ # If nothing is passed to to_filter, getting the jurisdictions list
252
+ to_filter = self.jurisdictions.copy() if to_filter is None else to_filter
253
+ # Normalizing the input being passed
254
+ input = self._normalize_input(input)
255
+ # This has the acceptable inputs we want to compare against
256
+ accepted_inputs = self._distinct_options(key)
257
+ # This will store the cleaned-up input
258
+ input_clean = []
259
+ for i in input:
260
+ # If the input is not a string, warn
261
+ if not isinstance(i, str):
262
+ warnings.warn(f"Invalid {key} filter: {i}. Only strings are considered valid, see documentation for details.")
263
+ # If the input is not in our list, warn the user
264
+ elif i not in accepted_inputs:
265
+ warnings.warn(f"Invalid {key} filter: {i}. Only the following inputs are considered valid: {accepted_inputs}.")
266
+ # Otherwise, add it to our list
267
+ else:
268
+ input_clean.append(i)
269
+ # Now we can use the clean input to filter
270
+ filtered = [j for j in to_filter if j[key] in input_clean]
271
+ # And returning the values
272
+ return self._process_return(filtered, to_return)
273
+
274
+ # Iterations for each categorical filter based on their respective inputs
275
+ def filter_region(self, region: Union[regions, List[regions]], to_filter=None, to_return="fips"):
276
+ return self._filter_categorical(region, "region", to_filter, to_return)
277
+
278
+ def filter_division(self, division: Union[divisions, List[divisions]], to_filter=None, to_return="fips"):
279
+ return self._filter_categorical(division, "division", to_filter, to_return)
280
+
281
+ def filter_omb(self, omb: Union[ombs, List[ombs]], to_filter=None, to_return="fips"):
282
+ return self._filter_categorical(omb, "omb", to_filter, to_return)
283
+
284
+ def filter_bea(self, bea: Union[beas, List[beas]], to_filter=None, to_return="fips"):
285
+ return self._filter_categorical(bea, "bea", to_filter, to_return)
286
+
287
+ # Function that processes the returning of a filtered jurisdiction
288
+ def _process_return(self, filter_juris, to_return):
289
+ # If the length is zero, warn!
290
+ if filter_juris is None or len(filter_juris) == 0:
291
+ warnings.warn(f"No matching entities found. Please refer to the documentation and double-check your filters.")
292
+ return None
293
+ if to_return is None:
294
+ to_return == "_ignore"
295
+ # Available options for to_return include fips, name, and abbr
296
+ elif to_return.lower() == "fips":
297
+ juris_return = [j["fips"] for j in filter_juris]
298
+ elif to_return.lower() == "name":
299
+ juris_return = [j["name"] for j in filter_juris]
300
+ elif to_return.lower() == "abbr":
301
+ juris_return = [j["abbr"] for j in filter_juris]
302
+ # Can also request that the entire object be returned, in which case nothing is done
303
+ # This will also happen if an invalid return object is passed
304
+ elif to_return.lower() not in ["object","dict","_ignore"]:
305
+ warnings.warn(f"Invalid to_return request: {to_return}. The entire object will be returned.")
306
+ juris_return = filter_juris.copy()
307
+ else:
308
+ juris_return = filter_juris.copy()
309
+
310
+ # Now, also processing the return request based on the length of the returned list
311
+ # If the length is zero, warn!
312
+ if len(juris_return) == 0:
313
+ warnings.warn(f"No matching entities found. Please refer to the documentation and double-check your filters.")
314
+ return None
315
+ # If only one element is returned, return the element itself, not a list
316
+ elif len(juris_return) == 1 and to_return != "_ignore":
317
+ return juris_return[0]
318
+ # Otherwise return the whole thing
319
+ else:
320
+ return juris_return
321
+
322
+ # Utility function to normalize a string that is passed to it
323
+ def _normalize_string(self, string, case="keep", nan="", spaces="_"):
324
+ string = string.strip()
325
+ if case == "lower":
326
+ string = string.lower()
327
+ string = re.sub(r"\W\S",nan,string)
328
+ string = re.sub(r"\s",spaces,string)
329
+ return string
330
+
331
+ # Utility function to convert a relevant non-list input to a list
332
+ def _normalize_input(self, input):
333
+ if not isinstance(input, (list, tuple)):
334
+ return [input]
335
+ else:
336
+ return input
@@ -20,7 +20,7 @@ def _validate_list(prop, val, list, none_ok=False):
20
20
  raise ValueError(f"'{val}' is not a valid value for {prop}, please provide a value in this list: {list}")
21
21
  return val
22
22
 
23
- def _validate_range(prop, val, min, max, none_ok=False):
23
+ def _validate_range(prop, val, min, max=None, none_ok=False):
24
24
  if none_ok==False and val is None:
25
25
  raise ValueError(f"None is not a valid value for {prop}, please provide a value between {min} and {max}")
26
26
  elif none_ok==True and val is None:
@@ -69,7 +69,7 @@ def _validate_tuple(prop, val, length, types, none_ok=False):
69
69
  raise ValueError(f"None is not a valid value for {prop}, please provide a tuple of length {length} instead")
70
70
  elif none_ok==True and val is None:
71
71
  return val
72
- elif type(val)!=tuple:
72
+ elif not isinstance(val, (tuple, list)):
73
73
  raise ValueError(f"{val} is not a valid value for {prop}, please provide a tuple of length {length} instead")
74
74
  elif len(val)!=length:
75
75
  raise ValueError(f"{val} is not a valid value for {prop}, please provide a tuple of length {length} instead")
@@ -117,7 +117,7 @@ def _validate_crs(prop, val, rotation_dict, none_ok=False):
117
117
  try:
118
118
  val = pyproj.CRS.from_user_input(val)
119
119
  except:
120
- raise Exception(f"Invalid CRS supplied ({val}), please provide a valid CRS input for PyProj instead")
120
+ raise Exception(f"Invalid CRS supplied ({val}), please provide a valid CRS input that PyProj can use instead")
121
121
  return val
122
122
 
123
123
  # A simpler validation function for CRSs
@@ -128,14 +128,14 @@ def _validate_projection(prop, val, none_ok=False):
128
128
  try:
129
129
  val = pyproj.CRS.from_user_input(val)
130
130
  except:
131
- raise Exception(f"Invalid CRS supplied ({val}), please provide a valid CRS input for PyProj instead")
131
+ raise Exception(f"Invalid CRS supplied ({val}) for {prop}, please provide a valid CRS input that PyProj can use instead")
132
132
  return val
133
133
 
134
- # It is specifically to apply another validation function to the items in a list
134
+ # This is specifically to apply another validation function to the items in a list
135
135
  # Ex. if we want to validate a LIST of colors instead of a single color
136
136
  def _validate_iterable(prop, val, func, kwargs=None):
137
137
  # Making sure we wrap everything in a list
138
- if type(val) not in [tuple, list]:
138
+ if not isinstance(val, (tuple, list)):
139
139
  val = [val]
140
140
  # Then, we apply our validation func with optional kwargs to each item in said list, relying on it to return an error value
141
141
  if kwargs is not None:
@@ -148,21 +148,57 @@ def _validate_iterable(prop, val, func, kwargs=None):
148
148
  v = func(v)
149
149
  return val
150
150
 
151
- # This is specifically to apply multiple validation functions to a value, if needed
151
+ # This is to check for the structure of a dictionary-like object
152
+ def _validate_keys(prop, val, keys, none_ok=False):
153
+ if none_ok==False and val is None:
154
+ raise ValueError(f"None is not a valid value for {prop}, please provide a dictionary with keys {keys} instead")
155
+ elif none_ok==True and val is None:
156
+ return val
157
+ elif not isinstance(val, (dict)):
158
+ raise ValueError(f"{val} is not a valid value for {prop}, please provide a dictionary with keys {keys} instead")
159
+ else:
160
+ for k in val.keys():
161
+ if k not in keys:
162
+ raise ValueError(f"{k} is not a valid key for the items in {prop}, please provide a dictionary with keys {keys} instead")
163
+ return val
164
+
165
+ # This is to apply multiple validation functions to a value, if needed - only one needs to pass
152
166
  # Ex. If an item can be a string OR a list of strings, we can use this to validate it
153
- # "labels":{"func":_validate_multiple, "kwargs":{"funcs":[_validate_type, _validate_list], "kwargs":[{"match":str, "none_ok":True}, {"list":["major","first_last","minor_all","minor_first"], "none_ok":True}]}}, # any string or any item in the list
154
- def _validate_multiple(prop, val, funcs, kwargs):
167
+ def _validate_or(prop, val, funcs, kwargs):
168
+ success = False
155
169
  # Simply iterate through each func and kwarg
156
170
  for f,k in zip(funcs,kwargs):
157
171
  # We wrap the attempts in a try block to suppress the errors
158
172
  try:
159
- v = f(prop=prop, val=v, **k)
173
+ val = f(prop=prop, val=val, **k)
160
174
  # If we pass, we can stop here and return the value
161
- return val
175
+ success = True
176
+ break
162
177
  except:
163
- continue
164
- # If we didn't return a value and exit the loop yet, then the passed value is incorrect, as we raise an error
165
- raise ValueError(f"{val} is not a valid value for {prop}, please provide check the documentation")
178
+ pass
179
+ if success == False:
180
+ # If we didn't return a value and exit the loop yet, then the passed value is incorrect, as we raise an error
181
+ raise ValueError(f"{val} is not a valid value for {prop}, please check the documentation")
182
+ else:
183
+ return val
184
+
185
+ # This is the same, but ALL need to pass
186
+ def _validate_and(prop, val, funcs, kwargs):
187
+ success = True
188
+ # Simply iterate through each func and kwarg
189
+ for f,k in zip(funcs,kwargs):
190
+ # We wrap the attempts in a try block to suppress the errors
191
+ try:
192
+ val = f(prop=prop, val=val, **k)
193
+ except:
194
+ # If we fail, we can stop here and return the value
195
+ success = False
196
+ break
197
+ if success == False:
198
+ # If we didn't return a value and exit the loop yet, then the passed value is incorrect, as we raise an error
199
+ raise ValueError(f"{val} is not a valid value for {prop}, please check the documentation")
200
+ else:
201
+ return val
166
202
 
167
203
  # This final one is used for keys that are not validated
168
204
  def _skip_validation(val, none_ok=False):
@@ -221,7 +257,7 @@ def _validate_dict(input_dict, default_dict, functions, to_validate=None, return
221
257
  def _validate(validate_dict, prop, val, return_val=True, kwargs={}):
222
258
  fd = validate_dict[prop]
223
259
  func = fd["func"]
224
- # Our custom functions always have this dictionary key in them, so we know what form they take
260
+ # Most of our custom functions always have this dictionary key in them, so we know what form they take
225
261
  if "kwargs" in fd:
226
262
  val = func(prop=prop, val=val, **(fd["kwargs"] | kwargs))
227
263
  # The matplotlib built-in functions DON'T have that, and only ever take the one value
@@ -0,0 +1,88 @@
1
+ ############################################################
2
+ # validation/inset_map.py contains all the main objects
3
+ # for checking inputs passed to class definitions
4
+ ############################################################
5
+
6
+ ### IMPORTING PACKAGES ###
7
+
8
+ # Geo packages
9
+ import matplotlib.axes
10
+ import pyproj
11
+ # Graphical packages
12
+ import matplotlib
13
+ # matplotlib's useful validation functions
14
+ import matplotlib.rcsetup
15
+ # The types we use in this script
16
+ from typing import TypedDict, Literal
17
+ # Finally, the validation functions
18
+ from . import functions as vf
19
+
20
+ ### ALL ###
21
+ # This code tells other packages what to import if not explicitly stated
22
+ __all__ = [
23
+ "_TYPE_INSET", "_VALIDATE_INSET",
24
+ "_TYPE_EXTENT", "_VALIDATE_EXTENT",
25
+ "_TYPE_DETAIL", "_VALIDATE_DETAIL",
26
+ ]
27
+
28
+ ### TYPE HINTS ###
29
+ # This section of the code is for defining structured dictionaries and lists
30
+ # for the inputs necessary for object creation we've created (such as the style dictionaries)
31
+ # so that intellisense can help with autocompletion
32
+
33
+ class _TYPE_INSET(TypedDict, total=False):
34
+ size: int | float | tuple[int | float, int | float] | list[int | float, int | float] # each int or float should be between 0 and inf
35
+ pad: int | float | tuple[int | float, int | float] | list[int | float, int | float] # each int or float should be between 0 and inf
36
+ coords: tuple[int | float, int | float] | list[int | float, int | float] # each int or float should be between -inf and inf
37
+
38
+ class _TYPE_EXTENT(TypedDict, total=False):
39
+ pax: matplotlib.axes.Axes # any Matplotlib Axes
40
+ bax: matplotlib.axes.Axes # any Matplotlib Axes
41
+ pcrs: str | int | pyproj.CRS # should be a valid cartopy or pyproj crs, or a string or int that can be converted to that
42
+ bcrs: str | int | pyproj.CRS # should be a valid cartopy or pyproj crs, or a string or int that can be converted to that
43
+ straighten: bool # either true or false
44
+ pad: float | int # any positive float or integer
45
+ plot: bool # either true or false
46
+ to_return: Literal["shape","patch","fig","ax"] | None # any item in the list, or None if nothing should be returned
47
+ facecolor: str # a color to use for the face of the box
48
+ linecolor: str # a color to use for the edge of the box
49
+ alpha: float | int # any positive float or integer
50
+ linewidth: float | int # any positive float or integer
51
+
52
+ class _TYPE_DETAIL(TypedDict, total=False):
53
+ to_return: Literal["connectors", "lines"] | None # any item in the list, or None if nothing should be returned
54
+ connector_color: str # a color to use for the face of the box
55
+ connector_width: float | int # any positive float or integer
56
+
57
+ ### VALIDITY DICTS ###
58
+ # These compile the functions in validation/functions, as well as matplotlib's built-in validity functions
59
+ # into dictionaries that can be used to validate all the inputs to a dictionary at once
60
+
61
+ _VALIDATE_INSET = {
62
+ "location":{"func":vf._validate_list, "kwargs":{"list":["upper right", "upper left", "lower left", "lower right", "center left", "center right", "lower center", "upper center", "center"]}},
63
+ "size":{"func":vf._validate_or, "kwargs":{"funcs":[vf._validate_range, vf._validate_and], "kwargs":[{"min":0, "none_ok":True}, {"funcs":[vf._validate_tuple, vf._validate_iterable], "kwargs":[{"length":2, "types":[float, int]}, {"func":vf._validate_range, "kwargs":{"min":0}}]}]}}, # between 0 and inf, or a two-tuple of (x,y) size, each between 0 and inf
64
+ "pad":{"func":vf._validate_or, "kwargs":{"funcs":[vf._validate_range, vf._validate_and], "kwargs":[{"min":0, "none_ok":True}, {"funcs":[vf._validate_tuple, vf._validate_iterable], "kwargs":[{"length":2, "types":[float, int]}, {"func":vf._validate_range, "kwargs":{"min":0}}]}]}}, # between 0 and inf, or a two-tuple of (x,y) size, each between 0 and inf
65
+ "coords":{"func":vf._validate_tuple, "kwargs":{"length":2, "types":[float, int], "none_ok":True}}, # a two-tuple of coordinates where you want to place the inset map
66
+ "to_plot":{"func":vf._validate_iterable, "kwargs":{"func":vf._validate_keys, "kwargs":{"keys":["data","kwargs"], "none_ok":True}}}, # a list of dictionaries, where each contains "data" and "kwargs" keys
67
+ }
68
+
69
+ _VALIDATE_EXTENT = {
70
+ "pax":{"func":vf._validate_type, "kwargs":{"match":matplotlib.axes.Axes}}, # any Matplotlib Axes
71
+ "bax":{"func":vf._validate_type, "kwargs":{"match":matplotlib.axes.Axes}}, # any Matplotlib Axes
72
+ "pcrs":{"func":vf._validate_projection, "kwargs":{"none_ok":False}}, # any valid projection input for PyProj
73
+ "bcrs":{"func":vf._validate_projection, "kwargs":{"none_ok":False}}, # any valid projection input for PyProj
74
+ "straighten":{"func":vf._validate_type, "kwargs":{"match":bool}}, # true or false
75
+ "pad":{"func":vf._validate_range, "kwargs":{"min":0}}, # any positive number
76
+ "plot":{"func":vf._validate_type, "kwargs":{"match":bool}}, # true or false
77
+ "facecolor":{"func":matplotlib.rcsetup.validate_color}, # any color value for matplotlib
78
+ "linecolor":{"func":matplotlib.rcsetup.validate_color}, # any color value for matplotlib
79
+ "alpha":{"func":vf._validate_range, "kwargs":{"min":0}}, # any positive number
80
+ "linewidth":{"func":vf._validate_range, "kwargs":{"min":0}}, # any positive number
81
+ "to_return":{"func":vf._validate_list, "kwargs":{"list":["shape", "patch", "fig", "ax"], "none_ok":True}}, # any value in this list
82
+ }
83
+
84
+ _VALIDATE_DETAIL = {
85
+ "to_return":{"func":vf._validate_list, "kwargs":{"list":["connectors", "lines"], "none_ok":True}}, # any value in this list
86
+ "connector_color":{"func":matplotlib.rcsetup.validate_color}, # any color value for matplotlib
87
+ "connector_width":{"func":vf._validate_range, "kwargs":{"min":0}}, # any positive number
88
+ }
@@ -90,7 +90,7 @@ class _TYPE_ROTATION(TypedDict, total=False):
90
90
  coords: Tuple[float | int, float | int] # only required if degrees is None: should be a tuple of coordinates in the relevant reference window
91
91
 
92
92
  ### VALIDITY DICTS ###
93
- # These compile the functions above^, as well as matplotlib's built-in validity functions
93
+ # These compile the functions in validation/functions, as well as matplotlib's built-in validity functions
94
94
  # into dictionaries that can be used to validate all the inputs to a dictionary at once
95
95
 
96
96
  _VALIDATE_PRIMARY = {
@@ -7,12 +7,10 @@
7
7
 
8
8
  # Geo packages
9
9
  import pyproj
10
- # Graphical packages
11
- import matplotlib
12
10
  # matplotlib's useful validation functions
13
11
  import matplotlib.rcsetup
14
12
  # The types we use in this script
15
- from typing import Tuple, TypedDict, Literal, get_args
13
+ from typing import TypedDict, Literal, get_args
16
14
  # Finally, the validation functions
17
15
  from . import functions as vf
18
16
 
@@ -161,7 +159,7 @@ class _TYPE_AOB(TypedDict, total=False):
161
159
  # bbox_transform: None # NOTE: currently unvalidated, use at your own risk!
162
160
 
163
161
  ### VALIDITY DICTS ###
164
- # These compile the functions above^, as well as matplotlib's built-in validity functions
162
+ # These compile the functions in validation/functions, as well as matplotlib's built-in validity functions
165
163
  # into dictionaries that can be used to validate all the inputs to a dictionary at once
166
164
 
167
165
  _VALIDATE_PRIMARY = {
@@ -173,7 +171,7 @@ _VALID_BAR_TICK_LOC = get_args(_TYPE_BAR.__annotations__["tick_loc"])
173
171
  _VALID_BAR_MINOR_TYPE = get_args(_TYPE_BAR.__annotations__["minor_type"])
174
172
 
175
173
  _VALIDATE_BAR = {
176
- "projection":{"func":vf._validate_projection}, # must be a valid CRS
174
+ "projection":{"func":vf._validate_projection, "kwargs":{"none_ok":False}}, # must be a valid CRS
177
175
  "unit":{"func":vf._validate_list, "kwargs":{"list":list(units_standard.keys()), "none_ok":True}}, # any of the listed unit values are accepted
178
176
  "rotation":{"func":vf._validate_range, "kwargs":{"min":-360, "max":360, "none_ok":True}}, # between -360 and 360 degrees
179
177
  "max":{"func":vf._validate_range, "kwargs":{"min":0, "max":None, "none_ok":True}}, # between 0 and inf