From 3e31b6f12680e5dfa1b3e8a173d6a508fe4aa44d Mon Sep 17 00:00:00 2001 From: Jakob Pinterits Date: Thu, 31 Oct 2024 16:54:56 +0000 Subject: [PATCH] random small fixes all over the place --- rio/app.py | 2 +- rio/byte_serving.py | 2 +- rio/components/component.py | 8 +-- rio/components/flow_container.py | 4 +- rio/components/fundamental_component.py | 2 +- rio/components/plot.py | 1 + rio/dialog.py | 2 +- rio/language_info.py | 6 +- rio/maybes.py | 8 ++- rio/utils.py | 94 +++++++++++++++++-------- rio/warnings.py | 2 +- 11 files changed, 85 insertions(+), 46 deletions(-) diff --git a/rio/app.py b/rio/app.py index 3b0b05b8..c6105ba7 100644 --- a/rio/app.py +++ b/rio/app.py @@ -459,7 +459,7 @@ class App: fastapi_app = app.as_fastapi() ``` - You can then run this app with uvicorn: + You can then run this app via `uvicorn`: ```sh uvicorn my_app:fastapi_app diff --git a/rio/byte_serving.py b/rio/byte_serving.py index f3bf6b0b..8bad00bf 100644 --- a/rio/byte_serving.py +++ b/rio/byte_serving.py @@ -94,7 +94,7 @@ def range_requests_response( } if media_type is None: - # There have been issues with javascript files because browsers insist + # There have been issues with JavaScript files because browsers insist # on the mime type "text/javascript", but some PCs aren't configured # correctly and return "text/plain". So we purposely avoid using # `mimetypes.guess_type` for javascript files. diff --git a/rio/components/component.py b/rio/components/component.py index e60b5ef6..e8f85858 100644 --- a/rio/components/component.py +++ b/rio/components/component.py @@ -655,7 +655,7 @@ class Component(abc.ABC, metaclass=ComponentMeta): the values of `margin`, `margin_x` and `margin_left`. """ - return utils.first_non_null( + return utils.first_non_none( self.margin_left, self.margin_x, self.margin, @@ -669,7 +669,7 @@ class Component(abc.ABC, metaclass=ComponentMeta): the values of `margin`, `margin_y` and `margin_top`. """ - return utils.first_non_null( + return utils.first_non_none( self.margin_top, self.margin_y, self.margin, @@ -683,7 +683,7 @@ class Component(abc.ABC, metaclass=ComponentMeta): the values of `margin`, `margin_right` and `margin_x`. """ - return utils.first_non_null( + return utils.first_non_none( self.margin_right, self.margin_x, self.margin, @@ -697,7 +697,7 @@ class Component(abc.ABC, metaclass=ComponentMeta): the values of `margin`, `margin_y` and `margin_bottom`. """ - return utils.first_non_null( + return utils.first_non_none( self.margin_bottom, self.margin_y, self.margin, diff --git a/rio/components/flow_container.py b/rio/components/flow_container.py index d32b10b8..a3e98314 100644 --- a/rio/components/flow_container.py +++ b/rio/components/flow_container.py @@ -134,12 +134,12 @@ class FlowContainer(FundamentalComponent): def _custom_serialize_(self) -> JsonDoc: result: JsonDoc = { - "row_spacing": utils.first_non_null( + "row_spacing": utils.first_non_none( self.row_spacing, self.spacing, 0, ), - "column_spacing": utils.first_non_null( + "column_spacing": utils.first_non_none( self.column_spacing, self.spacing, 0, diff --git a/rio/components/fundamental_component.py b/rio/components/fundamental_component.py index 042458f2..9b52367d 100644 --- a/rio/components/fundamental_component.py +++ b/rio/components/fundamental_component.py @@ -69,7 +69,7 @@ document.head.appendChild(style); class FundamentalComponent(Component): # Unique id for identifying this class in the frontend. This is initialized - # in `Component.__init_subclass__`. + # in `__init_subclass__`. _unique_id_: t.ClassVar[str] def build(self) -> rio.Component: diff --git a/rio/components/plot.py b/rio/components/plot.py index 241d52b8..e83162b0 100644 --- a/rio/components/plot.py +++ b/rio/components/plot.py @@ -217,6 +217,7 @@ class Plot(FundamentalComponent): # Matplotlib (+ Seaborn) elif isinstance(figure, maybes.MATPLOTLIB_GRAPH_TYPES): + # Seaborn "figures" are actually matplotlib `Axes` objects if isinstance(figure, maybes.MATPLOTLIB_AXES_TYPES): figure = figure.figure diff --git a/rio/dialog.py b/rio/dialog.py index a90af1f8..714b3d43 100644 --- a/rio/dialog.py +++ b/rio/dialog.py @@ -25,7 +25,7 @@ class Dialog(t.Generic[T]): def __init__(self) -> None: raise RuntimeError( - "Dialogs cannot be instantiated directly. To create a dialog, call `self.session.show_custom_dialog` inside of a component's event handler." + "Dialogs cannot be instantiated directly. To create a dialog, call `await self.session.show_custom_dialog(...)` inside of a component's event handler." ) @classmethod diff --git a/rio/language_info.py b/rio/language_info.py index b3c87a02..5261aab5 100644 --- a/rio/language_info.py +++ b/rio/language_info.py @@ -2,7 +2,7 @@ # and Sunday being 6. # # The most common first day of the week is Monday, and has been omitted from -# the list - use it as default values for missing languages. +# the list - use it as default value for missing languages. # # These were exported from Babel on 2024-06-01: # @@ -291,9 +291,9 @@ def get_week_start_day(language_tag: str) -> int: The result is a zero-based index, with Monday being 0 and Sunday being 6. """ - # According to stackoverflow some browsers (you know it's safari) send + # According to stackoverflow some browsers (you just know it's safari) send # invalid, lowercase tags. Normalize the tag to avoid issues. language_tag = language_tag.lower() - # Then look up, defaulting to Monday + # Look up the value, defaulting to Monday return _RFC_TO_WEEK_START_DAY.get(language_tag, 0) diff --git a/rio/maybes.py b/rio/maybes.py index ce5b1096..2abffa8f 100644 --- a/rio/maybes.py +++ b/rio/maybes.py @@ -41,7 +41,8 @@ PLOTLY_GRAPH_TYPES: tuple[type[plotly.graph_objects.Figure], ...] = () MATPLOTLIB_GRAPH_TYPES: tuple[type, ...] = () MATPLOTLIB_AXES_TYPES: tuple[type[matplotlib.axes.Axes], ...] = () -# This is a mapping of "weird" types to the "canonical" type, like `{np.int8: int}` +# This is a mapping of "weird" types to the "canonical" type, like `{np.int8: +# int}` TYPE_NORMALIZERS: dict[type[T], t.Callable[[T], T]] = {} # type: ignore @@ -51,12 +52,15 @@ def initialize(force: bool = False) -> None: is not automatically done on module load, to make sure any needed modules have already been imported - some functionality is not initialized if those other modules aren't used. + + If `force` is `True` everything is initialized even if it was already + initialized previously. """ global _IS_INITIALIZED global FLOAT_TYPES, INT_TYPES, BOOL_TYPES, STR_TYPES global NUMPY_ARRAY_TYPES global PANDAS_DATAFRAME_TYPES, POLARS_DATAFRAME_TYPES - global PLOTLY_GRAPH_TYPES, MATPLOTLIB_GRAPH_TYPES + global PLOTLY_GRAPH_TYPES, MATPLOTLIB_GRAPH_TYPES, MATPLOTLIB_AXES_TYPES # Already initialized? if _IS_INITIALIZED and not force: diff --git a/rio/utils.py b/rio/utils.py index d3331422..401e3fbd 100644 --- a/rio/utils.py +++ b/rio/utils.py @@ -28,8 +28,26 @@ __all__ = [ ] -# Sentinel type/value for when no value is provided and `None` would be -# unacceptable +# Sentinel type & value for when no value is provided and `None` would be +# unacceptable. +# +# To use this, use `NotGiven` as the type hint and `NOT_GIVEN` as the default +# value. Then, for checking the value, use `isinstance(value, NotGiven)`. While +# `value is NOT_GIVEN` would work, it is not recommended because it makes the +# type checker believe that the value could be another instance of `NotGiven`, +# even though there are no other instances. +# +# ## Example +# +# ```python +# def my_function(value: NotGiven = NOT_GIVEN) -> None: +# if isinstance(value, NotGiven): +# raise ValueError("You fool!") +# +# # Do something with the value +# ``` + + class NotGiven: pass @@ -76,7 +94,7 @@ ASSET_MANAGER: imy.assets.AssetManager = imy.assets.AssetManager( ) -# Precompiled regexes +# Precompiled regex patterns MARKDOWN_ESCAPE = re.compile(r"([\\`\*_\{\}\[\]\(\)#\+\-.!])") MARKDOWN_CODE_ESCAPE = re.compile(r"([\\`])") @@ -114,11 +132,11 @@ class FileInfo: """ Contains information about a file. - When asking the user to select a file, this class is used to represent the - file. It contains metadata about the file, and can also be used to access - the file's contents. + When asking the user to pick a file, this class is used to represent the + chosen file. It contains metadata about the file, and can also be used to + access the file's contents. - Be careful when running your app as a webserver, since files will need to be + Be careful when running your app as a website, since files will need to be uploaded by the user, which is a potentially very slow operation. @@ -144,6 +162,8 @@ class FileInfo: Reads and returns the entire file as a `bytes` object. If you know that the file is text, consider using `read_text` instead. """ + # TODO: Files are currently read in their entirety, immediately. Change + # that so that they are only fetched once this function is called. return self._contents async def read_text(self, *, encoding: str = "utf-8") -> str: @@ -154,13 +174,15 @@ class FileInfo: using the given `encoding`. If you don't know that the file is valid text, use `read_bytes` instead. + ## Parameters encoding: The encoding to use when decoding the file. + ## Raises - UnicodeDecodeError: The file could not be decoded using the given + `UnicodeDecodeError`: If the file could not be decoded using the given `encoding`. """ return self._contents.decode(encoding) @@ -212,7 +234,7 @@ def make_url_relative(base: URL, other: URL) -> URL: `other` is not a child of `base`. This will never generate URLs containing `..`. If those would be needed a - `ValueError` is raised. + `ValueError` is raised instead. """ # Verify the URLs have the same scheme and host if base.scheme != other.scheme: @@ -294,7 +316,7 @@ def port_is_free(host: str, port: int) -> bool: try: sock.bind((host, port)) return True - except OSError as err: + except OSError: return False @@ -305,7 +327,7 @@ def ensure_valid_port(host: str, port: int | None) -> int: return port -def first_non_null(*values: T | None) -> T: +def first_non_none(*values: T | None) -> T: """ Returns the first non-`None` value, or raises a `ValueError` if all values are `None`. @@ -410,6 +432,10 @@ def normalize_url(url: rio.URL) -> rio.URL: def is_python_script(path: Path) -> bool: + """ + Guesses whether the path points to a Python script, based on the path's file + extension. + """ return path.suffix in (".py", ".pyc", ".pyd", ".pyo", ".pyw") @@ -425,27 +451,30 @@ def normalize_file_type(file_type: str) -> str: This is best-effort. If the input type is invalid or unknown, the cleaned input may not be accurate. - Examples: - >>> standardize_file_type("pdf") - 'pdf' - >>> standardize_file_type(".PDF") - 'pdf' - >>> standardize_file_type("*.pdf") - 'pdf' - >>> standardize_file_type("application/pdf") - 'pdf' + ## Examples + + ```py + >>> standardize_file_type("pdf") + 'pdf' + >>> standardize_file_type(".PDF") + 'pdf' + >>> standardize_file_type("*.pdf") + 'pdf' + >>> standardize_file_type("application/pdf") + 'pdf' + ``` """ # Normalize the input string file_type = file_type.lower().strip() # If this is a MIME type, guess the extension if "/" in file_type: - guessed_type = mimetypes.guess_extension(file_type, strict=False) + guessed_suffix = mimetypes.guess_extension(file_type, strict=False) - if guessed_type is None: + if guessed_suffix is None: file_type = file_type.rsplit("/", 1)[-1] else: - file_type = guessed_type.lstrip(".") + file_type = guessed_suffix.lstrip(".") # If it isn't a MIME type, convert it to one anyway. Some file types have # multiple commonly used extensions. This will always map them to the same @@ -455,22 +484,27 @@ def normalize_file_type(file_type: str) -> str: f"file.{file_type}", strict=False ) - if guessed_type is None: - file_type = file_type.lstrip(".*") - else: + file_type = file_type.lstrip(".*") + + if guessed_type is not None: guessed_type = mimetypes.guess_extension( guessed_type, strict=False, ) - assert guessed_type is not None - file_type = guessed_type.lstrip(".") + # Yes, this really happens on some systems. For some reason, we can + # get the type for a suffix, but not the suffix for the same type. + if guessed_type is not None: + file_type = guessed_type.lstrip(".") # Done return file_type -def soft_sort(elements: list[T], key: t.Callable[[T], int | None]) -> None: +def soft_sort( + elements: list[T], + key: t.Callable[[T], int | None], +) -> None: """ Sorts the given list in-place, allowing for `None` values in the key. @@ -486,7 +520,7 @@ def soft_sort(elements: list[T], key: t.Callable[[T], int | None]) -> None: Note that there is one special case where this may not work as intended: If two items have the same key assigned they obviously cannot be placed at the same position. In that case the original order between them is preserved, - and followup items shifted as needed. + and subsequent items shifted as needed. """ # To ensure we can keep the original order, add the current index to all diff --git a/rio/warnings.py b/rio/warnings.py index f7ff4d0a..bc5290b2 100644 --- a/rio/warnings.py +++ b/rio/warnings.py @@ -1,6 +1,6 @@ class RioDeprecationWarning(Warning): """ - The user used/did something that's deprecated. + The user used functionality that has been deprecated. """