From 98fae89319095cc93ffaebb444199576ce3761d3 Mon Sep 17 00:00:00 2001 From: Elijah Ahianyo Date: Thu, 17 Aug 2023 18:23:09 +0000 Subject: [PATCH] Automatic Install FNM and Node for Windows: (#1566) --- poetry.lock | 108 +++++++++++++++++++++++++++++++--- pyproject.toml | 1 + reflex/constants.py | 41 ++++++++++--- reflex/utils/build.py | 2 + reflex/utils/exec.py | 5 +- reflex/utils/prerequisites.py | 96 +++++++++++++++++++----------- tests/test_testing.py | 2 +- tests/test_utils.py | 40 +++++++++---- 8 files changed, 228 insertions(+), 67 deletions(-) diff --git a/poetry.lock b/poetry.lock index e63ae2ea1..e57e6287c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,9 +1,10 @@ -# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. [[package]] name = "alembic" version = "1.11.1" description = "A database migration tool for SQLAlchemy." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -25,6 +26,7 @@ tz = ["python-dateutil"] name = "anyio" version = "3.7.1" description = "High level compatibility layer for multiple asynchronous event loop implementations" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -47,6 +49,7 @@ trio = ["trio (<0.22)"] name = "async-timeout" version = "4.0.2" description = "Timeout context manager for asyncio programs" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -61,6 +64,7 @@ typing-extensions = {version = ">=3.6.5", markers = "python_version < \"3.8\""} name = "asynctest" version = "0.13.0" description = "Enhance the standard unittest package with features for testing asyncio libraries" +category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -72,6 +76,7 @@ files = [ name = "attrs" version = "23.1.0" description = "Classes Without Boilerplate" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -93,6 +98,7 @@ tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pyte name = "bidict" version = "0.22.1" description = "The bidirectional mapping library for Python." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -109,6 +115,7 @@ test = ["hypothesis", "pytest", "pytest-benchmark[histogram]", "pytest-cov", "py name = "black" version = "22.12.0" description = "The uncompromising code formatter." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -145,6 +152,7 @@ uvloop = ["uvloop (>=0.15.2)"] name = "certifi" version = "2023.7.22" description = "Python package for providing Mozilla's CA Bundle." +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -156,6 +164,7 @@ files = [ name = "cffi" version = "1.15.1" description = "Foreign Function Interface for Python calling C code." +category = "dev" optional = false python-versions = "*" files = [ @@ -232,6 +241,7 @@ pycparser = "*" name = "cfgv" version = "3.3.1" description = "Validate configuration and produce human readable error messages." +category = "dev" optional = false python-versions = ">=3.6.1" files = [ @@ -243,6 +253,7 @@ files = [ name = "click" version = "8.1.6" description = "Composable command line interface toolkit" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -258,6 +269,7 @@ importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} name = "cloudpickle" version = "2.2.1" description = "Extended pickling support for Python objects" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -269,6 +281,7 @@ files = [ name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." +category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ @@ -280,6 +293,7 @@ files = [ name = "coverage" version = "7.2.7" description = "Code coverage measurement for Python" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -355,6 +369,7 @@ toml = ["tomli"] name = "darglint" version = "1.8.1" description = "A utility for ensuring Google-style docstrings stay up to date with the source code." +category = "dev" optional = false python-versions = ">=3.6,<4.0" files = [ @@ -366,6 +381,7 @@ files = [ name = "distlib" version = "0.3.7" description = "Distribution utilities" +category = "dev" optional = false python-versions = "*" files = [ @@ -377,6 +393,7 @@ files = [ name = "exceptiongroup" version = "1.1.2" description = "Backport of PEP 654 (exception groups)" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -391,6 +408,7 @@ test = ["pytest (>=6)"] name = "fastapi" version = "0.96.1" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -412,6 +430,7 @@ test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==23.1.0)", "coverage[toml] (>=6 name = "filelock" version = "3.12.2" description = "A platform independent file lock." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -427,6 +446,7 @@ testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "p name = "greenlet" version = "2.0.2" description = "Lightweight in-process concurrent programming" +category = "main" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" files = [ @@ -500,6 +520,7 @@ test = ["objgraph", "psutil"] name = "gunicorn" version = "20.1.0" description = "WSGI HTTP Server for UNIX" +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -520,6 +541,7 @@ tornado = ["tornado (>=0.2)"] name = "h11" version = "0.14.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -534,6 +556,7 @@ typing-extensions = {version = "*", markers = "python_version < \"3.8\""} name = "httpcore" version = "0.17.3" description = "A minimal low-level HTTP client." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -545,16 +568,17 @@ files = [ anyio = ">=3.0,<5.0" certifi = "*" h11 = ">=0.13,<0.15" -sniffio = "==1.*" +sniffio = ">=1.0.0,<2.0.0" [package.extras] http2 = ["h2 (>=3,<5)"] -socks = ["socksio (==1.*)"] +socks = ["socksio (>=1.0.0,<2.0.0)"] [[package]] name = "httpx" version = "0.24.1" description = "The next generation HTTP client." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -570,14 +594,15 @@ sniffio = "*" [package.extras] brotli = ["brotli", "brotlicffi"] -cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] -socks = ["socksio (==1.*)"] +socks = ["socksio (>=1.0.0,<2.0.0)"] [[package]] name = "identify" version = "2.5.26" description = "File identification library for Python" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -592,6 +617,7 @@ license = ["ukkonen"] name = "idna" version = "3.4" description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -603,6 +629,7 @@ files = [ name = "importlib-metadata" version = "6.7.0" description = "Read metadata from Python packages" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -623,6 +650,7 @@ testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs name = "importlib-resources" version = "5.12.0" description = "Read resources from Python packages" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -641,6 +669,7 @@ testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-chec name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -652,6 +681,7 @@ files = [ name = "jinja2" version = "3.1.2" description = "A very fast and expressive template engine." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -669,6 +699,7 @@ i18n = ["Babel (>=2.7)"] name = "mako" version = "1.2.4" description = "A super-fast templating language that borrows the best ideas from the existing templating languages." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -689,6 +720,7 @@ testing = ["pytest"] name = "markdown-it-py" version = "2.2.0" description = "Python port of markdown-it. Markdown parsing, done right!" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -714,6 +746,7 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] name = "markupsafe" version = "2.1.3" description = "Safely add untrusted strings to HTML/XML markup." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -773,6 +806,7 @@ files = [ name = "mdurl" version = "0.1.2" description = "Markdown URL utilities" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -784,6 +818,7 @@ files = [ name = "mypy-extensions" version = "1.0.0" description = "Type system extensions for programs checked with the mypy type checker." +category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -795,6 +830,7 @@ files = [ name = "nodeenv" version = "1.8.0" description = "Node.js virtual environment builder" +category = "dev" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" files = [ @@ -809,6 +845,7 @@ setuptools = "*" name = "numpy" version = "1.21.6" description = "NumPy is the fundamental package for array computing with Python." +category = "dev" optional = false python-versions = ">=3.7,<3.11" files = [ @@ -849,6 +886,7 @@ files = [ name = "numpy" version = "1.24.4" description = "Fundamental package for array computing in Python" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -886,6 +924,7 @@ files = [ name = "numpy" version = "1.25.2" description = "Fundamental package for array computing in Python" +category = "dev" optional = false python-versions = ">=3.9" files = [ @@ -920,6 +959,7 @@ files = [ name = "outcome" version = "1.2.0" description = "Capture the outcome of Python function calls." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -934,6 +974,7 @@ attrs = ">=19.2.0" name = "packaging" version = "23.1" description = "Core utilities for Python packages" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -945,6 +986,7 @@ files = [ name = "pandas" version = "1.1.5" description = "Powerful data structures for data analysis, time series, and statistics" +category = "dev" optional = false python-versions = ">=3.6.1" files = [ @@ -986,6 +1028,7 @@ test = ["hypothesis (>=3.58)", "pytest (>=4.0.2)", "pytest-xdist"] name = "pandas" version = "1.5.3" description = "Powerful data structures for data analysis, time series, and statistics" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1034,6 +1077,7 @@ test = ["hypothesis (>=5.5.3)", "pytest (>=6.0)", "pytest-xdist (>=1.31)"] name = "pathspec" version = "0.11.2" description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1045,6 +1089,7 @@ files = [ name = "platformdirs" version = "3.10.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1063,6 +1108,7 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-co name = "plotly" version = "5.15.0" description = "An open-source, interactive data visualization library for Python" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1078,6 +1124,7 @@ tenacity = ">=6.2.0" name = "pluggy" version = "1.2.0" description = "plugin and hook calling mechanisms for python" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1096,6 +1143,7 @@ testing = ["pytest", "pytest-benchmark"] name = "pre-commit" version = "3.3.3" description = "A framework for managing and maintaining multi-language pre-commit hooks." +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1114,6 +1162,7 @@ virtualenv = ">=20.10.0" name = "psutil" version = "5.9.5" description = "Cross-platform lib for process and system monitoring in Python." +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1140,6 +1189,7 @@ test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] name = "pycparser" version = "2.21" description = "C parser in Python" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1151,6 +1201,7 @@ files = [ name = "pydantic" version = "1.10.12" description = "Data validation and settings management using python type hints" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1203,6 +1254,7 @@ email = ["email-validator (>=1.0.3)"] name = "pygments" version = "2.15.1" description = "Pygments is a syntax highlighting package written in Python." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1217,6 +1269,7 @@ plugins = ["importlib-metadata"] name = "pyright" version = "1.1.318" description = "Command line wrapper for pyright" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1236,6 +1289,7 @@ dev = ["twine (>=3.4.1)"] name = "pysocks" version = "1.7.1" description = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information." +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1248,6 +1302,7 @@ files = [ name = "pytest" version = "7.4.0" description = "pytest: simple powerful testing with Python" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1271,6 +1326,7 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no name = "pytest-asyncio" version = "0.20.3" description = "Pytest support for asyncio" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1290,6 +1346,7 @@ testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy name = "pytest-cov" version = "4.1.0" description = "Pytest plugin for measuring coverage." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1308,6 +1365,7 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale name = "pytest-mock" version = "3.11.1" description = "Thin-wrapper around the mock package for easier use with pytest" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1325,6 +1383,7 @@ dev = ["pre-commit", "pytest-asyncio", "tox"] name = "python-dateutil" version = "2.8.2" description = "Extensions to the standard Python datetime module" +category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ @@ -1339,6 +1398,7 @@ six = ">=1.5" name = "python-dotenv" version = "0.13.0" description = "Add .env support to your django/flask apps in development and deployments" +category = "main" optional = false python-versions = "*" files = [ @@ -1353,6 +1413,7 @@ cli = ["click (>=5.0)"] name = "python-engineio" version = "4.5.1" description = "Engine.IO server and client for Python" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1369,6 +1430,7 @@ docs = ["sphinx"] name = "python-multipart" version = "0.0.5" description = "A streaming multipart parser for Python" +category = "main" optional = false python-versions = "*" files = [ @@ -1382,6 +1444,7 @@ six = ">=1.4.0" name = "python-socketio" version = "5.8.0" description = "Socket.IO server and client for Python" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1401,6 +1464,7 @@ client = ["requests (>=2.21.0)", "websocket-client (>=0.54.0)"] name = "pytz" version = "2023.3" description = "World timezone definitions, modern and historical" +category = "dev" optional = false python-versions = "*" files = [ @@ -1412,6 +1476,7 @@ files = [ name = "pyyaml" version = "6.0.1" description = "YAML parser and emitter for Python" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1461,6 +1526,7 @@ files = [ name = "redis" version = "4.6.0" description = "Python client for Redis database and key-value store" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1481,6 +1547,7 @@ ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)" name = "rich" version = "13.5.1" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +category = "main" optional = false python-versions = ">=3.7.0" files = [ @@ -1500,6 +1567,7 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] name = "ruff" version = "0.0.244" description = "An extremely fast Python linter, written in Rust." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1525,6 +1593,7 @@ files = [ name = "selenium" version = "4.10.0" description = "" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1542,6 +1611,7 @@ urllib3 = {version = ">=1.26,<3", extras = ["socks"]} name = "setuptools" version = "68.0.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1558,6 +1628,7 @@ testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs ( name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -1569,6 +1640,7 @@ files = [ name = "sniffio" version = "1.3.0" description = "Sniff out which async library your code is running under" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1580,6 +1652,7 @@ files = [ name = "sortedcontainers" version = "2.4.0" description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" +category = "dev" optional = false python-versions = "*" files = [ @@ -1591,6 +1664,7 @@ files = [ name = "sqlalchemy" version = "1.4.41" description = "Database Abstraction Library" +category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ @@ -1638,7 +1712,7 @@ files = [ ] [package.dependencies] -greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and (platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\")"} +greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and platform_machine == \"aarch64\" or python_version >= \"3\" and platform_machine == \"ppc64le\" or python_version >= \"3\" and platform_machine == \"x86_64\" or python_version >= \"3\" and platform_machine == \"amd64\" or python_version >= \"3\" and platform_machine == \"AMD64\" or python_version >= \"3\" and platform_machine == \"win32\" or python_version >= \"3\" and platform_machine == \"WIN32\""} importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [package.extras] @@ -1666,6 +1740,7 @@ sqlcipher = ["sqlcipher3-binary"] name = "sqlalchemy2-stubs" version = "0.0.2a35" description = "Typing Stubs for SQLAlchemy 1.4" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1680,6 +1755,7 @@ typing-extensions = ">=3.7.4" name = "sqlmodel" version = "0.0.8" description = "SQLModel, SQL databases in Python, designed for simplicity, compatibility, and robustness." +category = "main" optional = false python-versions = ">=3.6.1,<4.0.0" files = [ @@ -1696,6 +1772,7 @@ sqlalchemy2-stubs = "*" name = "starlette" version = "0.27.0" description = "The little ASGI library that shines." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1714,6 +1791,7 @@ full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyam name = "starlette-admin" version = "0.9.0" description = "Fast, beautiful and extensible administrative interface framework for Starlette/FastApi applications" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1736,6 +1814,7 @@ test = ["aiomysql (>=0.1.1,<0.2.0)", "aiosqlite (>=0.17.0,<0.20.0)", "arrow (>=1 name = "tenacity" version = "8.2.2" description = "Retry code until it succeeds" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1750,6 +1829,7 @@ doc = ["reno", "sphinx", "tornado (>=4.5)"] name = "toml" version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -1761,6 +1841,7 @@ files = [ name = "tomli" version = "2.0.1" description = "A lil' TOML parser" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1772,6 +1853,7 @@ files = [ name = "trio" version = "0.22.2" description = "A friendly Python library for async concurrency and I/O" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1792,6 +1874,7 @@ sortedcontainers = "*" name = "trio-websocket" version = "0.10.3" description = "WebSocket library for Trio" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1808,6 +1891,7 @@ wsproto = ">=0.14" name = "typed-ast" version = "1.5.5" description = "a fork of Python 2 and 3 ast modules with type comment support" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1858,6 +1942,7 @@ files = [ name = "typer" version = "0.4.2" description = "Typer, build great CLIs. Easy to code. Based on Python type hints." +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1878,6 +1963,7 @@ test = ["black (>=22.3.0,<23.0.0)", "coverage (>=5.2,<6.0)", "isort (>=5.0.6,<6. name = "typing-extensions" version = "4.7.1" description = "Backported and Experimental Type Hints for Python 3.7+" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1889,6 +1975,7 @@ files = [ name = "urllib3" version = "2.0.4" description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1909,6 +1996,7 @@ zstd = ["zstandard (>=0.18.0)"] name = "uvicorn" version = "0.20.0" description = "The lightning-fast ASGI server." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1928,6 +2016,7 @@ standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", name = "virtualenv" version = "20.24.2" description = "Virtual Python Environment builder" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1948,6 +2037,7 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess name = "watchdog" version = "2.3.1" description = "Filesystem events monitoring" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1988,6 +2078,7 @@ watchmedo = ["PyYAML (>=3.10)"] name = "watchfiles" version = "0.19.0" description = "Simple, modern and high performance file watching and code reload in python." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2022,6 +2113,7 @@ anyio = ">=3.0.0" name = "websockets" version = "10.4" description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2100,6 +2192,7 @@ files = [ name = "wsproto" version = "1.2.0" description = "WebSockets state-machine based protocol implementation" +category = "dev" optional = false python-versions = ">=3.7.0" files = [ @@ -2114,6 +2207,7 @@ h11 = ">=0.9.0,<1" name = "zipp" version = "3.15.0" description = "Backport of pathlib-compatible object wrapper for zip files" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2128,4 +2222,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "2.0" python-versions = "^3.7" -content-hash = "ac27016107e8a033aa39d9a712d3ef685132e22ede599a26214b17da6ff35829" +content-hash = "ba03a445b7e59587264636a98b52595e020ae58570ac79b0fb1b9564c9769c59" diff --git a/pyproject.toml b/pyproject.toml index b670cd0b3..aa6b74cc4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,6 +46,7 @@ starlette-admin = "^0.9.0" python-dotenv = "^0.13.0" importlib-metadata = {version = "^6.7.0", python = ">=3.7, <3.8"} alembic = "^1.11.1" +platformdirs = "^3.10.0" [tool.poetry.group.dev.dependencies] pytest = "^7.1.2" diff --git a/reflex/constants.py b/reflex/constants.py index 83eff1baf..e60c2a758 100644 --- a/reflex/constants.py +++ b/reflex/constants.py @@ -8,12 +8,16 @@ from enum import Enum from types import SimpleNamespace from typing import Any, Type +from platformdirs import PlatformDirs + # importlib is only available for Python 3.8+ so we need the backport for Python 3.7 try: from importlib import metadata except ImportError: import importlib_metadata as metadata # pyright: ignore[reportMissingImports] +IS_WINDOWS = platform.system() == "Windows" + def get_value(key: str, default: Any = None, type_: Type = str) -> Type: """Get the value for the constant. @@ -48,7 +52,17 @@ VERSION = metadata.version(MODULE_NAME) # Files and directories used to init a new project. # The directory to store reflex dependencies. -REFLEX_DIR = os.path.expandvars(os.path.join("$HOME", f".{MODULE_NAME}")) +REFLEX_DIR = ( + # on windows, we use C:/Users//AppData/Local/reflex. + PlatformDirs(MODULE_NAME, False).user_data_dir + if IS_WINDOWS + else os.path.expandvars( + os.path.join( + "$HOME", + f".{MODULE_NAME}", + ), + ) +) # The root directory of the reflex library. ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # The name of the assets directory. @@ -79,29 +93,38 @@ BUN_INSTALL_URL = "https://bun.sh/install" # NVM / Node config. # The NVM version. NVM_VERSION = "0.39.1" +# The FNM version. +FNM_VERSION = "1.35.1" # The Node version. NODE_VERSION = "18.17.0" # The minimum required node version. NODE_VERSION_MIN = "16.8.0" # The directory to store nvm. NVM_DIR = os.path.join(REFLEX_DIR, ".nvm") +# The directory to store fnm. +FNM_DIR = os.path.join(REFLEX_DIR, "fnm") +# The fnm executable binary. +FNM_EXE = os.path.join(FNM_DIR, "fnm.exe") # The nvm path. NVM_PATH = os.path.join(NVM_DIR, "nvm.sh") # The node bin path. -NODE_BIN_PATH = os.path.join(NVM_DIR, "versions", "node", f"v{NODE_VERSION}", "bin") +NODE_BIN_PATH = ( + os.path.join(NVM_DIR, "versions", "node", f"v{NODE_VERSION}", "bin") + if not IS_WINDOWS + else os.path.join(FNM_DIR, "node-versions", f"v{NODE_VERSION}", "installation") +) # The default path where node is installed. -NODE_PATH = ( - "node" if platform.system() == "Windows" else os.path.join(NODE_BIN_PATH, "node") -) +NODE_PATH = os.path.join(NODE_BIN_PATH, "node.exe" if IS_WINDOWS else "node") # The default path where npm is installed. -NPM_PATH = ( - "npm" if platform.system() == "Windows" else os.path.join(NODE_BIN_PATH, "npm") -) +NPM_PATH = os.path.join(NODE_BIN_PATH, "npm") # The URL to the nvm install script. NVM_INSTALL_URL = ( f"https://raw.githubusercontent.com/nvm-sh/nvm/v{NVM_VERSION}/install.sh" ) - +# The URL to the fnm release binary +FNM_WINDOWS_INSTALL_URL = ( + f"https://github.com/Schniz/fnm/releases/download/v{FNM_VERSION}/fnm-windows.zip" +) # The frontend directories in a project. # The web folder where the NextJS app is compiled to. WEB_DIR = ".web" diff --git a/reflex/utils/build.py b/reflex/utils/build.py index 68e0ace73..d7127524a 100644 --- a/reflex/utils/build.py +++ b/reflex/utils/build.py @@ -124,6 +124,7 @@ def export( process = processes.new_process( [prerequisites.get_package_manager(), "run", command], cwd=constants.WEB_DIR, + shell=constants.IS_WINDOWS, ) processes.show_progress("Creating Production Build", process, checkpoints) @@ -201,6 +202,7 @@ def setup_frontend( ], cwd=constants.WEB_DIR, stdout=subprocess.DEVNULL, + shell=constants.IS_WINDOWS, ) diff --git a/reflex/utils/exec.py b/reflex/utils/exec.py index f7d7bae3d..a87238102 100644 --- a/reflex/utils/exec.py +++ b/reflex/utils/exec.py @@ -30,8 +30,7 @@ def run_process_and_launch_url( run_command: The command to run. """ process = processes.new_process( - run_command, - cwd=constants.WEB_DIR, + run_command, cwd=constants.WEB_DIR, shell=constants.IS_WINDOWS ) if process.stdout: @@ -137,7 +136,7 @@ def run_backend_prod( str(port), f"{app_name}:{constants.APP_VAR}", ] - if prerequisites.IS_WINDOWS + if constants.IS_WINDOWS else [ *constants.RUN_BACKEND_PROD, "--bind", diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py index 7c704fe16..b6cba6c10 100644 --- a/reflex/utils/prerequisites.py +++ b/reflex/utils/prerequisites.py @@ -5,10 +5,10 @@ from __future__ import annotations import glob import json import os -import platform import re import sys import tempfile +import zipfile from fileinput import FileInput from pathlib import Path from types import ModuleType @@ -24,8 +24,6 @@ from reflex import constants, model from reflex.config import get_config from reflex.utils import console, path_ops, processes -IS_WINDOWS = platform.system() == "Windows" - def check_node_version() -> bool: """Check the version of Node.js. @@ -44,7 +42,7 @@ def check_node_version() -> bool: # Compare the version numbers return ( current_version >= version.parse(constants.NODE_VERSION_MIN) - if IS_WINDOWS + if constants.IS_WINDOWS else current_version == version.parse(constants.NODE_VERSION) ) @@ -85,11 +83,9 @@ def get_install_package_manager() -> str: Returns: The path to the package manager. """ - get_config() - # On Windows, we use npm instead of bun. - if IS_WINDOWS: - return get_windows_package_manager() + if constants.IS_WINDOWS: + return constants.NPM_PATH # On other platforms, we use bun. return get_config().bun_path @@ -102,10 +98,6 @@ def get_package_manager() -> str: Returns: The path to the package manager. """ - get_config() - - if IS_WINDOWS: - return get_windows_package_manager() return constants.NPM_PATH @@ -279,24 +271,59 @@ def download_and_run(url: str, *args, show_status: bool = False, **env): show(f"Installing {url}", process) -def install_node(): - """Install nvm and nodejs for use by Reflex. - Independent of any existing system installations. +def download_and_extract_fnm_zip(url: str): + """Download and run a script. + + Args: + url: The url of the fnm release zip binary. Raises: - Exit: if installation failed + Exit: If an error occurs while downloading or extracting the FNM zip. """ - if IS_WINDOWS: - # See if existing node is good enough. - # On Windows, this must be installed manually, outside of Reflex. - if not check_node_version(): - # We don't currently support auto install of node on Windows - # because NVM is not supported there - console.error( - f"Node.js version {constants.NODE_VERSION} or higher is required to run Reflex." - ) - raise typer.Exit(1) - return + # TODO: make this OS agnostic + # Download the zip file + console.debug(f"Downloading {url}") + fnm_zip_file = f"{constants.FNM_DIR}\\fnm_windows.zip" + # Function to download and extract the FNM zip release + try: + # Download the FNM zip release + # TODO: show progress to improve UX + with httpx.stream("GET", url, follow_redirects=True) as response: + response.raise_for_status() + with open(fnm_zip_file, "wb") as output_file: + for chunk in response.iter_bytes(): + output_file.write(chunk) + + # Extract the downloaded zip file + with zipfile.ZipFile(fnm_zip_file, "r") as zip_ref: + zip_ref.extractall(constants.FNM_DIR) + + console.debug("FNM for Windows downloaded and extracted successfully.") + except Exception as e: + console.error(f"An error occurred while downloading fnm package: {e}") + raise typer.Exit(1) from e + finally: + # Clean up the downloaded zip file + path_ops.rm(fnm_zip_file) + + +def install_node(): + """Install nvm and nodejs for use by Reflex. + Independent of any existing system installations. + """ + if constants.IS_WINDOWS: + path_ops.mkdir(constants.FNM_DIR) + if not os.path.exists(constants.FNM_EXE): + download_and_extract_fnm_zip(constants.FNM_WINDOWS_INSTALL_URL) + + # Install node. + process = processes.new_process( + [ + "powershell", + "-Command", + f'& "{constants.FNM_EXE}" install {constants.NODE_VERSION} --fnm-dir "{constants.FNM_DIR}"', + ], + ) else: # All other platforms (Linux, MacOS) # TODO we can skip installation if check_node_version() checks out # Create the nvm directory and install. @@ -314,7 +341,7 @@ def install_node(): ], env=env, ) - processes.show_status("Installing node", process) + processes.show_status("Installing node", process) def install_bun(): @@ -324,7 +351,7 @@ def install_bun(): FileNotFoundError: If required packages are not found. """ # Bun is not supported on Windows. - if IS_WINDOWS: + if constants.IS_WINDOWS: console.debug("Skipping bun installation on Windows.") return @@ -352,6 +379,7 @@ def install_frontend_packages(): process = processes.new_process( [get_install_package_manager(), "install", "--loglevel", "silly"], cwd=constants.WEB_DIR, + shell=constants.IS_WINDOWS, ) processes.show_status("Installing base frontend packages", process) @@ -361,6 +389,7 @@ def install_frontend_packages(): process = processes.new_process( [get_install_package_manager(), "add", *packages], cwd=constants.WEB_DIR, + shell=constants.IS_WINDOWS, ) processes.show_status("Installing custom frontend packages", process) @@ -375,7 +404,7 @@ def check_initialized(frontend: bool = True): Exit: If the app is not initialized. """ has_config = os.path.exists(constants.CONFIG_FILE) - has_reflex_dir = not frontend or IS_WINDOWS or os.path.exists(constants.REFLEX_DIR) + has_reflex_dir = not frontend or os.path.exists(constants.REFLEX_DIR) has_web_dir = not frontend or os.path.exists(constants.WEB_DIR) # Check if the app is initialized. @@ -393,7 +422,7 @@ def check_initialized(frontend: bool = True): raise typer.Exit(1) # Print a warning for Windows users. - if IS_WINDOWS: + if constants.IS_WINDOWS: console.warn( """Windows Subsystem for Linux (WSL) is recommended for improving initial install times.""" ) @@ -440,7 +469,7 @@ def validate_bun(): def validate_frontend_dependencies(): """Validate frontend dependencies to ensure they meet requirements.""" - if IS_WINDOWS: + if constants.IS_WINDOWS: return return validate_bun() @@ -448,8 +477,7 @@ def validate_frontend_dependencies(): def initialize_frontend_dependencies(): """Initialize all the frontend dependencies.""" # Create the reflex directory. - if not IS_WINDOWS: - path_ops.mkdir(constants.REFLEX_DIR) + path_ops.mkdir(constants.REFLEX_DIR) # validate dependencies before install validate_frontend_dependencies() # Install the frontend dependencies. diff --git a/tests/test_testing.py b/tests/test_testing.py index 1cf476d39..e24c7224f 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -1,6 +1,6 @@ """Unit tests for the included testing tools.""" +from reflex.constants import IS_WINDOWS from reflex.testing import AppHarness -from reflex.utils.prerequisites import IS_WINDOWS def test_app_harness(tmp_path): diff --git a/tests/test_utils.py b/tests/test_utils.py index e3e496a74..2f6b85d74 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -513,24 +513,42 @@ def test_app_default_name(tmp_path, mocker): prerequisites.get_default_app_name() -def test_node_install_windows(mocker): +def test_node_install_windows(tmp_path, mocker): """Require user to install node manually for windows if node is not installed. Args: + tmp_path: Test working dir. mocker: Pytest mocker object. """ - mocker.patch("reflex.utils.prerequisites.IS_WINDOWS", True) - mocker.patch("reflex.utils.prerequisites.check_node_version", return_value=False) + fnm_root_path = tmp_path / "reflex" / "fnm" + fnm_exe = fnm_root_path / "fnm.exe" - with pytest.raises(typer.Exit): - prerequisites.install_node() + mocker.patch("reflex.utils.prerequisites.constants.FNM_DIR", fnm_root_path) + mocker.patch("reflex.utils.prerequisites.constants.FNM_EXE", fnm_exe) + mocker.patch("reflex.utils.prerequisites.constants.IS_WINDOWS", True) + mocker.patch("reflex.utils.processes.new_process") + mocker.patch("reflex.utils.processes.stream_logs") + + class Resp(Base): + status_code = 200 + text = "test" + + mocker.patch("httpx.stream", return_value=Resp()) + download = mocker.patch("reflex.utils.prerequisites.download_and_extract_fnm_zip") + mocker.patch("reflex.utils.prerequisites.zipfile.ZipFile") + mocker.patch("reflex.utils.prerequisites.path_ops.rm") + + prerequisites.install_node() + + assert fnm_root_path.exists() + download.assert_called_once() def test_node_install_unix(tmp_path, mocker): nvm_root_path = tmp_path / ".reflex" / ".nvm" mocker.patch("reflex.utils.prerequisites.constants.NVM_DIR", nvm_root_path) - mocker.patch("reflex.utils.prerequisites.IS_WINDOWS", False) + mocker.patch("reflex.utils.prerequisites.constants.IS_WINDOWS", False) class Resp(Base): status_code = 200 @@ -556,13 +574,12 @@ def test_bun_install_without_unzip(mocker): """ mocker.patch("reflex.utils.path_ops.which", return_value=None) mocker.patch("os.path.exists", return_value=False) - mocker.patch("reflex.utils.prerequisites.IS_WINDOWS", False) + mocker.patch("reflex.utils.prerequisites.constants.IS_WINDOWS", False) with pytest.raises(FileNotFoundError): prerequisites.install_bun() -# from @pytest.mark.parametrize("is_windows", [True, False]) def test_create_reflex_dir(mocker, is_windows): """Test that a reflex directory is created on initializing frontend @@ -572,7 +589,7 @@ def test_create_reflex_dir(mocker, is_windows): mocker: Pytest mocker object. is_windows: Whether platform is windows. """ - mocker.patch("reflex.utils.prerequisites.IS_WINDOWS", is_windows) + mocker.patch("reflex.utils.prerequisites.constants.IS_WINDOWS", is_windows) mocker.patch("reflex.utils.prerequisites.processes.run_concurrently", mocker.Mock()) mocker.patch("reflex.utils.prerequisites.initialize_web_directory", mocker.Mock()) create_cmd = mocker.patch( @@ -581,7 +598,4 @@ def test_create_reflex_dir(mocker, is_windows): prerequisites.initialize_frontend_dependencies() - if is_windows: - assert not create_cmd.called - else: - assert create_cmd.called + assert create_cmd.called