diff --git a/.github/workflows/.hatch-run.yml b/.github/workflows/.hatch-run.yml index 1b21e4202..1630378b9 100644 --- a/.github/workflows/.hatch-run.yml +++ b/.github/workflows/.hatch-run.yml @@ -38,15 +38,15 @@ jobs: runs-on: ${{ fromJson(inputs.runs-on-array) }} runs-on: ${{ matrix.runs-on }} steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: - node-version: "14.x" + node-version: "23.x" registry-url: ${{ inputs.node-registry-url }} - name: Pin NPM Version run: npm install -g npm@8.19.3 - name: Use Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install Python Dependencies diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 7337f505b..f9f9431c6 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -4,27 +4,27 @@ name: deploy-docs on: - push: - branches: - - "main" - tags: - - "*" + push: + branches: + - "main" + tags: + - "*" jobs: - deploy-documentation: - runs-on: ubuntu-latest - steps: - - name: Check out src from Git - uses: actions/checkout@v2 - - name: Get history and tags for SCM versioning to work - run: | - git fetch --prune --unshallow - git fetch --depth=1 origin +refs/tags/*:refs/tags/* - - name: Login to Heroku Container Registry - run: echo ${{ secrets.HEROKU_API_KEY }} | docker login -u ${{ secrets.HEROKU_EMAIL }} --password-stdin registry.heroku.com - - name: Build Docker Image - run: docker build . --file docs/Dockerfile --tag registry.heroku.com/${{ secrets.HEROKU_APP_NAME }}/web - - name: Push Docker Image - run: docker push registry.heroku.com/${{ secrets.HEROKU_APP_NAME }}/web - - name: Deploy - run: HEROKU_API_KEY=${{ secrets.HEROKU_API_KEY }} heroku container:release web --app ${{ secrets.HEROKU_APP_NAME }} + deploy-documentation: + runs-on: ubuntu-latest + steps: + - name: Check out src from Git + uses: actions/checkout@v4 + - name: Get history and tags for SCM versioning to work + run: | + git fetch --prune --unshallow + git fetch --depth=1 origin +refs/tags/*:refs/tags/* + - name: Login to Heroku Container Registry + run: echo ${{ secrets.HEROKU_API_KEY }} | docker login -u ${{ secrets.HEROKU_EMAIL }} --password-stdin registry.heroku.com + - name: Build Docker Image + run: docker build . --file docs/Dockerfile --tag registry.heroku.com/${{ secrets.HEROKU_APP_NAME }}/web + - name: Push Docker Image + run: docker push registry.heroku.com/${{ secrets.HEROKU_APP_NAME }}/web + - name: Deploy + run: HEROKU_API_KEY=${{ secrets.HEROKU_API_KEY }} heroku container:release web --app ${{ secrets.HEROKU_APP_NAME }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e9271cbd5..8e523ce04 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -4,17 +4,17 @@ name: publish on: - release: - types: [published] + release: + types: [published] jobs: - publish: - uses: ./.github/workflows/.hatch-run.yml - with: - job-name: "publish" - hatch-run: "publish" - node-registry-url: "https://registry.npmjs.org" - secrets: - node-auth-token: ${{ secrets.NODE_AUTH_TOKEN }} - pypi-username: ${{ secrets.PYPI_USERNAME }} - pypi-password: ${{ secrets.PYPI_PASSWORD }} + publish: + uses: ./.github/workflows/.hatch-run.yml + with: + job-name: "publish" + hatch-run: "publish" + node-registry-url: "https://registry.npmjs.org" + secrets: + node-auth-token: ${{ secrets.NODE_AUTH_TOKEN }} + pypi-username: ${{ secrets.PYPI_USERNAME }} + pypi-password: ${{ secrets.PYPI_PASSWORD }} diff --git a/docs/source/about/changelog.rst b/docs/source/about/changelog.rst index 9fc13e015..04d426a6b 100644 --- a/docs/source/about/changelog.rst +++ b/docs/source/about/changelog.rst @@ -23,6 +23,7 @@ Unreleased - :pull:`1118` - `module_from_template` is broken with a recent release of `requests` - :pull:`1131` - `module_from_template` did not work when using Flask backend - :pull:`1200` - Fixed `UnicodeDecodeError` when using `reactpy.web.export` +- :pull:`1224` - Fixes needless unmounting of JavaScript components during each ReactPy render. **Added** @@ -40,12 +41,15 @@ Unreleased fragment to conditionally render an element by writing ``something if condition else html._()``. Now you can simply write ``something if condition else None``. +- :pull:`1210` - Move hooks from ``reactpy.backend.hooks`` into ``reactpy.core.hooks``. **Deprecated** - :pull:`1171` - The ``Stop`` exception. Recent releases of ``anyio`` have made this exception difficult to use since it now raises an ``ExceptionGroup``. This exception was primarily used for internal testing purposes and so is now deprecated. +- :pull:`1210` - Deprecate ``reactpy.backend.hooks`` since the hooks have been moved into + ``reactpy.core.hooks``. v1.0.2 diff --git a/pyproject.toml b/pyproject.toml index 775ab01a2..1745a3dfe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,12 +12,11 @@ detached = true dependencies = [ "invoke", # lint - "black==24.1.1", # Pin lint tools we don't control to avoid breaking changes - "ruff==0.0.278", # Pin lint tools we don't control to avoid breaking changes + "black==24.1.1", # Pin lint tools we don't control to avoid breaking changes + "ruff==0.0.278", # Pin lint tools we don't control to avoid breaking changes "toml", - "flake8==7.0.0", # Pin lint tools we don't control to avoid breaking changes + "flake8==7.0.0", # Pin lint tools we don't control to avoid breaking changes "flake8-pyproject", - "reactpy-flake8 >=0.7", # types "mypy", "types-toml", diff --git a/src/js/packages/@reactpy/client/src/components.tsx b/src/js/packages/@reactpy/client/src/components.tsx index 728c4cec7..2319f81c7 100644 --- a/src/js/packages/@reactpy/client/src/components.tsx +++ b/src/js/packages/@reactpy/client/src/components.tsx @@ -177,7 +177,7 @@ function useForceUpdate() { function useImportSource(model: ReactPyVdom): MutableRefObject { const vdomImportSource = model.importSource; - + const vdomImportSourceJsonString = JSON.stringify(vdomImportSource); const mountPoint = useRef(null); const client = React.useContext(ClientContext); const [binding, setBinding] = useState(null); @@ -203,7 +203,7 @@ function useImportSource(model: ReactPyVdom): MutableRefObject { binding.unmount(); } }; - }, [client, vdomImportSource, setBinding, mountPoint.current]); + }, [client, vdomImportSourceJsonString, setBinding, mountPoint.current]); // this effect must run every time in case the model has changed useEffect(() => { diff --git a/src/py/reactpy/pyproject.toml b/src/py/reactpy/pyproject.toml index 309248507..e5a2559b7 100644 --- a/src/py/reactpy/pyproject.toml +++ b/src/py/reactpy/pyproject.toml @@ -12,9 +12,7 @@ readme = "README.md" requires-python = ">=3.9" license = "MIT" keywords = ["react", "javascript", "reactpy", "component"] -authors = [ - { name = "Ryan Morshead", email = "ryan.morshead@gmail.com" }, -] +authors = [{ name = "Ryan Morshead", email = "ryan.morshead@gmail.com" }] classifiers = [ "Development Status :: 4 - Beta", "Programming Language :: Python", @@ -39,10 +37,7 @@ dependencies = [ [project.optional-dependencies] all = ["reactpy[starlette,sanic,fastapi,flask,tornado,testing]"] -starlette = [ - "starlette >=0.13.6", - "uvicorn[standard] >=0.19.0", -] +starlette = ["starlette >=0.13.6", "uvicorn[standard] >=0.19.0"] sanic = [ "sanic >=21", "sanic-cors", @@ -50,22 +45,10 @@ sanic = [ "setuptools", "uvicorn[standard] >=0.19.0", ] -fastapi = [ - "fastapi >=0.63.0", - "uvicorn[standard] >=0.19.0", -] -flask = [ - "flask", - "markupsafe>=1.1.1,<2.1", - "flask-cors", - "flask-sock", -] -tornado = [ - "tornado", -] -testing = [ - "playwright", -] +fastapi = ["fastapi >=0.63.0", "uvicorn[standard] >=0.19.0"] +flask = ["flask", "markupsafe>=1.1.1,<2.1", "flask-cors", "flask-sock"] +tornado = ["tornado"] +testing = ["playwright"] [project.urls] Source = "https://github.com/reactive-python/reactpy" @@ -101,21 +84,17 @@ cov-report = [ # "- coverage combine", "coverage report", ] -cov = [ - "test-cov {args}", - "cov-report", -] +cov = ["test-cov {args}", "cov-report"] [tool.hatch.envs.default.env-vars] -REACTPY_DEBUG_MODE="1" +REACTPY_DEBUG_MODE = "1" [tool.hatch.envs.lint] features = ["all"] dependencies = [ - "mypy>=1.0.0", + "mypy==1.8", "types-click", "types-tornado", - "types-pkg-resources", "types-flask", "types-requests", ] @@ -127,13 +106,8 @@ all = ["types"] [[tool.hatch.build.hooks.build-scripts.scripts]] work_dir = "../../js" out_dir = "reactpy/_static" -commands = [ - "npm ci", - "npm run build" -] -artifacts = [ - "app/dist/" -] +commands = ["npm ci", "npm run build"] +artifacts = ["app/dist/"] # --- Pytest --------------------------------------------------------------------------- @@ -159,9 +133,7 @@ warn_unused_ignores = true source_pkgs = ["reactpy"] branch = false parallel = false -omit = [ - "reactpy/__init__.py", -] +omit = ["reactpy/__init__.py"] [tool.coverage.report] fail_under = 100 @@ -174,6 +146,4 @@ exclude_lines = [ "if __name__ == .__main__.:", "if TYPE_CHECKING:", ] -omit = [ - "reactpy/__main__.py", -] +omit = ["reactpy/__main__.py"] diff --git a/src/py/reactpy/reactpy/__init__.py b/src/py/reactpy/reactpy/__init__.py index 49e357441..c47142cd8 100644 --- a/src/py/reactpy/reactpy/__init__.py +++ b/src/py/reactpy/reactpy/__init__.py @@ -1,5 +1,4 @@ from reactpy import backend, config, html, logging, sample, svg, types, web, widgets -from reactpy.backend.hooks import use_connection, use_location, use_scope from reactpy.backend.utils import run from reactpy.core import hooks from reactpy.core.component import component @@ -7,12 +6,15 @@ from reactpy.core.hooks import ( create_context, use_callback, + use_connection, use_context, use_debug_value, use_effect, + use_location, use_memo, use_reducer, use_ref, + use_scope, use_state, ) from reactpy.core.layout import Layout diff --git a/src/py/reactpy/reactpy/backend/flask.py b/src/py/reactpy/reactpy/backend/flask.py index faa979aa9..4401fb6f7 100644 --- a/src/py/reactpy/reactpy/backend/flask.py +++ b/src/py/reactpy/reactpy/backend/flask.py @@ -35,9 +35,9 @@ safe_client_build_dir_path, safe_web_modules_dir_path, ) -from reactpy.backend.hooks import ConnectionContext -from reactpy.backend.hooks import use_connection as _use_connection from reactpy.backend.types import Connection, Location +from reactpy.core.hooks import ConnectionContext +from reactpy.core.hooks import use_connection as _use_connection from reactpy.core.serve import serve_layout from reactpy.core.types import ComponentType, RootComponentConstructor from reactpy.utils import Ref diff --git a/src/py/reactpy/reactpy/backend/hooks.py b/src/py/reactpy/reactpy/backend/hooks.py index ee4ce1b5c..ef1b4a5cb 100644 --- a/src/py/reactpy/reactpy/backend/hooks.py +++ b/src/py/reactpy/reactpy/backend/hooks.py @@ -1,30 +1,45 @@ -from __future__ import annotations +from __future__ import annotations # nocov -from collections.abc import MutableMapping -from typing import Any +from collections.abc import MutableMapping # nocov +from typing import Any # nocov -from reactpy.backend.types import Connection, Location -from reactpy.core.hooks import create_context, use_context -from reactpy.core.types import Context +from reactpy._warnings import warn # nocov +from reactpy.backend.types import Connection, Location # nocov +from reactpy.core.hooks import ConnectionContext, use_context # nocov -# backend implementations should establish this context at the root of an app -ConnectionContext: Context[Connection[Any] | None] = create_context(None) - -def use_connection() -> Connection[Any]: +def use_connection() -> Connection[Any]: # nocov """Get the current :class:`~reactpy.backend.types.Connection`.""" + warn( + "The module reactpy.backend.hooks has been deprecated and will be deleted in the future. ", + "Call reactpy.use_connection instead.", + DeprecationWarning, + ) + conn = use_context(ConnectionContext) - if conn is None: # nocov + if conn is None: msg = "No backend established a connection." raise RuntimeError(msg) return conn -def use_scope() -> MutableMapping[str, Any]: +def use_scope() -> MutableMapping[str, Any]: # nocov """Get the current :class:`~reactpy.backend.types.Connection`'s scope.""" + warn( + "The module reactpy.backend.hooks has been deprecated and will be deleted in the future. ", + "Call reactpy.use_scope instead.", + DeprecationWarning, + ) + return use_connection().scope -def use_location() -> Location: +def use_location() -> Location: # nocov """Get the current :class:`~reactpy.backend.types.Connection`'s location.""" + warn( + "The module reactpy.backend.hooks has been deprecated and will be deleted in the future. ", + "Call reactpy.use_location instead.", + DeprecationWarning, + ) + return use_connection().location diff --git a/src/py/reactpy/reactpy/backend/sanic.py b/src/py/reactpy/reactpy/backend/sanic.py index 76eb0423e..d272fb4cf 100644 --- a/src/py/reactpy/reactpy/backend/sanic.py +++ b/src/py/reactpy/reactpy/backend/sanic.py @@ -24,9 +24,9 @@ safe_web_modules_dir_path, serve_with_uvicorn, ) -from reactpy.backend.hooks import ConnectionContext -from reactpy.backend.hooks import use_connection as _use_connection from reactpy.backend.types import Connection, Location +from reactpy.core.hooks import ConnectionContext +from reactpy.core.hooks import use_connection as _use_connection from reactpy.core.layout import Layout from reactpy.core.serve import RecvCoroutine, SendCoroutine, Stop, serve_layout from reactpy.core.types import RootComponentConstructor diff --git a/src/py/reactpy/reactpy/backend/starlette.py b/src/py/reactpy/reactpy/backend/starlette.py index 9bc68db47..20e2b4478 100644 --- a/src/py/reactpy/reactpy/backend/starlette.py +++ b/src/py/reactpy/reactpy/backend/starlette.py @@ -24,10 +24,10 @@ read_client_index_html, serve_with_uvicorn, ) -from reactpy.backend.hooks import ConnectionContext -from reactpy.backend.hooks import use_connection as _use_connection from reactpy.backend.types import Connection, Location from reactpy.config import REACTPY_WEB_MODULES_DIR +from reactpy.core.hooks import ConnectionContext +from reactpy.core.hooks import use_connection as _use_connection from reactpy.core.layout import Layout from reactpy.core.serve import RecvCoroutine, SendCoroutine, serve_layout from reactpy.core.types import RootComponentConstructor diff --git a/src/py/reactpy/reactpy/backend/tornado.py b/src/py/reactpy/reactpy/backend/tornado.py index 8f540ddb4..bd339c5b9 100644 --- a/src/py/reactpy/reactpy/backend/tornado.py +++ b/src/py/reactpy/reactpy/backend/tornado.py @@ -24,10 +24,10 @@ CommonOptions, read_client_index_html, ) -from reactpy.backend.hooks import ConnectionContext -from reactpy.backend.hooks import use_connection as _use_connection from reactpy.backend.types import Connection, Location from reactpy.config import REACTPY_WEB_MODULES_DIR +from reactpy.core.hooks import ConnectionContext +from reactpy.core.hooks import use_connection as _use_connection from reactpy.core.layout import Layout from reactpy.core.serve import serve_layout from reactpy.core.types import ComponentConstructor diff --git a/src/py/reactpy/reactpy/core/events.py b/src/py/reactpy/reactpy/core/events.py index f715b7e9d..2a193ec6b 100644 --- a/src/py/reactpy/reactpy/core/events.py +++ b/src/py/reactpy/reactpy/core/events.py @@ -109,7 +109,7 @@ def __init__( self.stop_propagation = stop_propagation self.target = target - def __eq__(self, other: Any) -> bool: + def __eq__(self, other: object) -> bool: undefined = object() for attr in ( "function", diff --git a/src/py/reactpy/reactpy/core/hooks.py b/src/py/reactpy/reactpy/core/hooks.py index 640cbf14c..0ece8cccf 100644 --- a/src/py/reactpy/reactpy/core/hooks.py +++ b/src/py/reactpy/reactpy/core/hooks.py @@ -1,7 +1,7 @@ from __future__ import annotations import asyncio -from collections.abc import Coroutine, Sequence +from collections.abc import Coroutine, MutableMapping, Sequence from logging import getLogger from types import FunctionType from typing import ( @@ -17,6 +17,7 @@ from typing_extensions import TypeAlias +from reactpy.backend.types import Connection, Location from reactpy.config import REACTPY_DEBUG_MODE from reactpy.core._life_cycle_hook import current_hook from reactpy.core.types import Context, Key, State, VdomDict @@ -248,6 +249,29 @@ def use_context(context: Context[_Type]) -> _Type: return provider.value +# backend implementations should establish this context at the root of an app +ConnectionContext: Context[Connection[Any] | None] = create_context(None) + + +def use_connection() -> Connection[Any]: + """Get the current :class:`~reactpy.backend.types.Connection`.""" + conn = use_context(ConnectionContext) + if conn is None: # nocov + msg = "No backend established a connection." + raise RuntimeError(msg) + return conn + + +def use_scope() -> MutableMapping[str, Any]: + """Get the current :class:`~reactpy.backend.types.Connection`'s scope.""" + return use_connection().scope + + +def use_location() -> Location: + """Get the current :class:`~reactpy.backend.types.Connection`'s location.""" + return use_connection().location + + class _ContextProvider(Generic[_Type]): def __init__( self, diff --git a/src/py/reactpy/reactpy/core/layout.py b/src/py/reactpy/reactpy/core/layout.py index f45becf7a..88cb2fa35 100644 --- a/src/py/reactpy/reactpy/core/layout.py +++ b/src/py/reactpy/reactpy/core/layout.py @@ -89,7 +89,7 @@ async def __aenter__(self) -> Layout: return self - async def __aexit__(self, *exc: Any) -> None: + async def __aexit__(self, *exc: object) -> None: root_csid = self._root_life_cycle_state_id root_model_state = self._model_states_by_life_cycle_state_id[root_csid] diff --git a/src/py/reactpy/reactpy/utils.py b/src/py/reactpy/reactpy/utils.py index 5624846a4..a20194902 100644 --- a/src/py/reactpy/reactpy/utils.py +++ b/src/py/reactpy/reactpy/utils.py @@ -43,7 +43,7 @@ def set_current(self, new: _RefValue) -> _RefValue: self.current = new return old - def __eq__(self, other: Any) -> bool: + def __eq__(self, other: object) -> bool: try: return isinstance(other, Ref) and (other.current == self.current) except AttributeError: diff --git a/src/py/reactpy/reactpy/web/templates/react.js b/src/py/reactpy/reactpy/web/templates/react.js index 5c6a45743..366be4fd0 100644 --- a/src/py/reactpy/reactpy/web/templates/react.js +++ b/src/py/reactpy/reactpy/web/templates/react.js @@ -17,11 +17,12 @@ export default ({ children, ...props }) => { }; export function bind(node, config) { + const root = ReactDOM.createRoot(node); return { create: (component, props, children) => React.createElement(component, wrapEventHandlers(props), ...children), - render: (element) => ReactDOM.render(element, node), - unmount: () => ReactDOM.unmountComponentAtNode(node), + render: (element) => root.render(element), + unmount: () => root.unmount() }; } diff --git a/src/py/reactpy/tests/test_backend/test_all.py b/src/py/reactpy/tests/test_backend/test_all.py index dc8ec1284..cd2f371f5 100644 --- a/src/py/reactpy/tests/test_backend/test_all.py +++ b/src/py/reactpy/tests/test_backend/test_all.py @@ -112,7 +112,7 @@ async def test_use_location(display: DisplayFixture): @poll async def poll_location(): """This needs to be async to allow the server to respond""" - return location.current + return getattr(location, "current", None) @reactpy.component def ShowRoute(): diff --git a/src/py/reactpy/tests/test_core/test_events.py b/src/py/reactpy/tests/test_core/test_events.py index 237c9d4ed..b6fea346a 100644 --- a/src/py/reactpy/tests/test_core/test_events.py +++ b/src/py/reactpy/tests/test_core/test_events.py @@ -193,7 +193,7 @@ def inner_click_no_op(event): clicked.current = True def outer_click_is_not_triggered(event): - raise AssertionError() + raise AssertionError outer = reactpy.html.div( {