diff --git a/docs/guide.md b/docs/guide.md index 4c93c5e..11dbbb2 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -19,14 +19,15 @@ If this flag is set, default values or user provided arguments will be used. The following table shows a list of questions, their default values, their arguments to provide answers in quiet mode, and an explanation: -| Question | Default | Argument | Explanation | -|----------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Choose a builder tool for the project | `rye` | `-b`
`--builder` | *Required:* The builder to use to package your pyproject file. Valid tools are `rye`, `hatch`, `pdm`, `build`, and `flit`. Ensure that the builder is available in the environment in which you run box. | -| Provide any optional dependencies for the project. | `None` | `--opt`
`--optional-deps` | *Optional:* Set any optional dependencies for the project. These are the dependencies that you would install in square brackets, e.g., via `pip install package_name[optional]`. If there are no optional dependencies, just hit enter. | -| Please type an app entry for the project or choose one from the list below | First entry in `pyproject.toml` for `[project.gui-scripts]`, if not available, then for `[project.scripts]`. If no entry points are given, the default value is set to `package_name:run`. | `-e`
`--entry` | *Required:* The entry point for the application. This is the command that will be used to start the application. If you have a `pyproject.toml` file, `box` will try and read potential entry points that you can select by passing it the digit of the list. You can also provide an entry point manually by typing it here. | -| Choose an entry type for the project. | `spec` | `-et`
`--entry-type` | *Required:* This specifies the type of the entry point that `PyApp` will use. `spec` (default) is an object reference, `module` for a python module, `script` for a python script, and `notebook` for a jupyter notebook. Details can be found [here](https://ofek.dev/pyapp/latest/config/#execution-mode). | -| Choose a python version to package the project with. | `3.12` | `--py`
`--python-version` | *Required:* The python version to package with your project. You can choose any python version from the list. More details can be found on the `PyApp` website [here](https://ofek.dev/pyapp/latest/config/#python-distribution). | -| Optional PyApp variables | `None` | `--opt-pyapp-vars` | *Optional:* Set any optional environmental variables that you can use in `PyApp` here by giving a list of variable name followed by the value for that variable. Note that the total number of arguments must be even. See the [`PyApp` documentation](https://ofek.dev/pyapp) for more information. If there are no optional variables, just hit enter. | +| Question | Default | Argument | Explanation | +|----------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Choose a builder tool for the project | `rye` | `-b`
`--builder` | *Required:* The builder to use to package your pyproject file. Valid tools are `rye`, `hatch`, `pdm`, `build`, and `flit`. Ensure that the builder is available in the environment in which you run box. | +| Provide any optional dependencies for the project. | `None` | `--opt`
`--optional-deps` | *Optional:* Set any optional dependencies for the project. These are the dependencies that you would install in square brackets, e.g., via `pip install package_name[optional]`. If there are no optional dependencies, just hit enter. | +| Is this a GUI project? | `False` | `--gui` flag to toggle to `True` | Packaging a GUI project and a CLI project take place slightly differently in PyApp. If you package a GUI project without setting this option, a console will be shown in Windows and macos along with your GUI. | | +| Please type an app entry for the project or choose one from the list below | First entry in `pyproject.toml` for `[project.gui-scripts]`, if not available, then for `[project.scripts]`. If no entry points are given, the default value is set to `package_name:run`. | `-e`
`--entry` | *Required:* The entry point for the application. This is the command that will be used to start the application. If you have a `pyproject.toml` file, `box` will try and read potential entry points that you can select by passing it the digit of the list. You can also provide an entry point manually by typing it here. | +| Choose an entry type for the project. | `spec` | `-et`
`--entry-type` | *Required:* This specifies the type of the entry point that `PyApp` will use. `spec` (default) is an object reference, `module` for a python module, `script` for a python script, and `notebook` for a jupyter notebook. Details can be found [here](https://ofek.dev/pyapp/latest/config/#execution-mode). | +| Choose a python version to package the project with. | `3.12` | `--py`
`--python-version` | *Required:* The python version to package with your project. You can choose any python version from the list. More details can be found on the `PyApp` website [here](https://ofek.dev/pyapp/latest/config/#python-distribution). | +| Optional PyApp variables | `None` | `--opt-pyapp-vars` | *Optional:* Set any optional environmental variables that you can use in `PyApp` here by giving a list of variable name followed by the value for that variable. Note that the total number of arguments must be even. See the [`PyApp` documentation](https://ofek.dev/pyapp) for more information. If there are no optional variables, just hit enter. | If you provided all the answers, your project should have been successfully initialized and print an according message. diff --git a/src/box/cli.py b/src/box/cli.py index 9f8fe7d..e5c6364 100644 --- a/src/box/cli.py +++ b/src/box/cli.py @@ -34,6 +34,12 @@ def cli(): @click.option( "-opt", "--optional-deps", help="Set optional dependencies for the project." ) +@click.option( + "--gui", + is_flag=True, + default=None, + help="Set the project as a GUI project. In quiet mode, this will default to `False`.", +) @click.option( "--opt-pyapp-vars", help=( @@ -58,7 +64,14 @@ def cli(): help="Set the python version to use with PyApp.", ) def init( - quiet, builder, optional_deps, entry, entry_type, python_version, opt_pyapp_vars + quiet, + builder, + optional_deps, + gui, + entry, + entry_type, + python_version, + opt_pyapp_vars, ): """Initialize a new project in the current folder.""" ut.check_pyproject() @@ -66,6 +79,7 @@ def init( quiet=quiet, builder=builder, optional_deps=optional_deps, + is_gui=gui, app_entry=entry, app_entry_type=entry_type, python_version=python_version, diff --git a/src/box/config.py b/src/box/config.py index e320256..56f7e2f 100644 --- a/src/box/config.py +++ b/src/box/config.py @@ -44,6 +44,11 @@ def is_box_project(self): except KeyError: return False + @property + def is_gui(self) -> bool: + """Return if the project is a GUI project.""" + return self._pyproject["tool"]["box"]["is_gui"] + @property def name(self) -> str: """Return the name of the project.""" diff --git a/src/box/initialization.py b/src/box/initialization.py index b5eadf9..f63ce4b 100644 --- a/src/box/initialization.py +++ b/src/box/initialization.py @@ -18,6 +18,7 @@ def __init__( quiet: bool = False, builder: str = None, optional_deps: str = None, + is_gui: bool = None, app_entry: str = None, app_entry_type: str = None, python_version: str = None, @@ -28,6 +29,7 @@ def __init__( :param quiet: Flag to suppress output :param builder: Builder tool to use. :param optional_deps: Optional dependencies for the project. + :param is_gui: Flag to set the project as a GUI project. :param app_entry: App entry for the project. :param app_entry_type: Entry type for the project in PyApp. :param python_version: Python version for the project. @@ -36,6 +38,7 @@ def __init__( self._quiet = quiet self._builder = builder self._optional_deps = optional_deps + self._is_gui = is_gui self._opt_paypp_vars = opt_pyapp_vars self._app_entry = app_entry self._app_entry_type = app_entry_type @@ -53,6 +56,7 @@ def initialize(self): """ self._set_builder() self._set_optional_deps() + self._set_is_gui() self._set_app_entry() self._set_app_entry_type() self._set_python_version() @@ -164,6 +168,21 @@ def _set_builder(self): # reload self._set_pyproj() + def _set_is_gui(self): + """Set if the project is a GUI project or not.""" + if self._is_gui is not None: + is_gui = self._is_gui + else: + if self._quiet: + try: + is_gui = self.pyproj.is_gui + except KeyError: + is_gui = False + else: + is_gui = click.confirm("Is this a GUI project?", default=False) + + pyproject_writer("is_gui", is_gui) + def _set_optional_deps(self): """Set optional dependencies for the project (if any).""" if (tmp := self.pyproj.optional_dependencies) is not None: diff --git a/src/box/packager.py b/src/box/packager.py index 639be85..9391ac4 100644 --- a/src/box/packager.py +++ b/src/box/packager.py @@ -233,6 +233,8 @@ def _set_env(self): optional_pyapp_vars = self.config.optional_pyapp_variables for key, value in optional_pyapp_vars.items(): os.environ[key] = value + if self.config.is_gui: + os.environ["PYAPP_IS_GUI"] = "1" # STATIC METHODS # @staticmethod diff --git a/tests/cli/test_cli_initialization.py b/tests/cli/test_cli_initialization.py index d980f05..2a119d6 100644 --- a/tests/cli/test_cli_initialization.py +++ b/tests/cli/test_cli_initialization.py @@ -11,7 +11,7 @@ @pytest.mark.parametrize( "app_entry", - ["\n\nhello\n\n3.8\nPYAPP_FULL_ISOLATION 1", "\ngui\n127\nhello\nmodule\n\n"], + ["\n\n\nhello\n\n3.8\nPYAPP_FULL_ISOLATION 1", "\ngui\n\n127\nhello\nmodule\n\n"], ) def test_initialize_project_app_entry_typed(rye_project_no_box, app_entry): """Initialize a new project.""" @@ -59,6 +59,9 @@ def test_initialize_project_app_entry_typed(rye_project_no_box, app_entry): # assert that default builder is set to rye assert pyproj.builder == "rye" + # assert not a gui project + assert not pyproj.is_gui + # assert default python version if py_version_exp == "": py_version_exp = ut.PYAPP_PYTHON_VERSIONS[-1] @@ -75,7 +78,8 @@ def test_initialize_project_app_entry_typed(rye_project_no_box, app_entry): assert pyproj.optional_pyapp_variables == exp_dict -def test_initialize_with_options(rye_project_no_box): +@pytest.mark.parametrize("gui", [True, False]) +def test_initialize_with_options(rye_project_no_box, gui): """Initialize a new project with options.""" py_version = "3.8" entry_point = "myapp:entry" @@ -83,25 +87,26 @@ def test_initialize_with_options(rye_project_no_box): optional_deps = "gui" builder = "hatch" + args = [ + "init", + "-e", + entry_point, + "-et", + entry_type, + "-py", + py_version, + "-opt", + optional_deps, + "-b", + builder, + "--opt-pyapp-vars", + "PYAPP_FULL_ISOLATION 1", + ] + if gui: + args.append("--gui") + runner = CliRunner() - result = runner.invoke( - cli, - [ - "init", - "-e", - entry_point, - "-et", - entry_type, - "-py", - py_version, - "-opt", - optional_deps, - "-b", - builder, - "--opt-pyapp-vars", - "PYAPP_FULL_ISOLATION 1", - ], - ) + result = runner.invoke(cli, args) assert result.exit_code == 0 assert "Project initialized." in result.output @@ -114,6 +119,7 @@ def test_initialize_with_options(rye_project_no_box): assert pyproj.builder == builder assert pyproj.python_version == py_version assert pyproj.optional_dependencies == optional_deps + assert pyproj.is_gui == gui assert pyproj.app_entry == entry_point assert pyproj.app_entry_type == entry_type assert pyproj.optional_pyapp_variables == {"PYAPP_FULL_ISOLATION": "1"} @@ -145,6 +151,7 @@ def test_initialize_project_again(rye_project_no_box): builder, "--opt-pyapp-vars", pyapp_vars, + "--gui", ], ) @@ -155,6 +162,7 @@ def test_initialize_project_again(rye_project_no_box): assert pyproj.builder == builder assert pyproj.python_version == py_version assert pyproj.optional_dependencies == optional_deps + assert pyproj.is_gui assert pyproj.app_entry == entry_point assert pyproj.app_entry_type == entry_type @@ -177,7 +185,7 @@ def test_initialize_project_quiet(rye_project_no_box): def test_initialize_project_builders(rye_project_no_box, builder): """Initialize a new project with a specific builder.""" runner = CliRunner() - result = runner.invoke(cli, ["init"], input=f"{builder}\n\nsome_entry") + result = runner.invoke(cli, ["init"], input=f"{builder}\n\n\nsome_entry") assert result.exit_code == 0 # assert that default builder is set to rye @@ -191,7 +199,7 @@ def test_initialize_project_wrong_number_of_pyapp_vars(rye_project_no_box): result = runner.invoke( cli, ["init"], - input="\n\nhello\n\n\nPYAPP_FULL_ISOLATION 1 2\nPYAPP_FULL_ISOLATION 1", + input="\n\n\nhello\n\n\nPYAPP_FULL_ISOLATION 1 2\nPYAPP_FULL_ISOLATION 1", ) assert result.exit_code == 0 diff --git a/tests/conftest.py b/tests/conftest.py index 2502acd..9a024f1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -54,6 +54,7 @@ def rye_project(tmp_path): pyproject_writer("builder", "rye") pyproject_writer("app_entry", "pkg:run") pyproject_writer("entry_type", "spec") + pyproject_writer("is_gui", False) pyproject_writer("python_version", "3.12") yield tmp_path diff --git a/tests/func/test_config.py b/tests/func/test_config.py index 327bfd7..0206e5c 100644 --- a/tests/func/test_config.py +++ b/tests/func/test_config.py @@ -67,6 +67,7 @@ def test_pyproject_parser_rye_project(rye_project): assert isinstance(parser.name, str) assert isinstance(parser.version, str) assert isinstance(parser.rye, dict) + assert isinstance(parser.is_gui, bool) def test_pyproject_writer_set_entry(tmp_path_chdir): diff --git a/tests/func/test_initialization.py b/tests/func/test_initialization.py index 9f60276..52c5b3d 100644 --- a/tests/func/test_initialization.py +++ b/tests/func/test_initialization.py @@ -7,20 +7,17 @@ from box.initialization import InitializeProject -def test_app_entry(rye_project_no_box): - """Set the configuration in `pyproject.toml` to rye.""" - init = InitializeProject(quiet=True) - init.initialize() - - pyproj = PyProjectParser() - assert pyproj.app_entry == f"{rye_project_no_box.name}:run" - - -def test_set_builder(rye_project_no_box): - """Set the configuration in `pyproject.toml` to rye.""" +def test_quiet_init(rye_project_no_box): + """Check default configuration upon a quiet init.""" init = InitializeProject(quiet=True) init.initialize() pyproj = PyProjectParser() assert pyproj.builder == "rye" + assert pyproj.optional_dependencies is None + assert not pyproj.is_gui + assert pyproj.app_entry == f"{rye_project_no_box.name}:run" + assert pyproj.app_entry_type == "spec" + assert pyproj.python_version == "3.12" + assert pyproj.optional_pyapp_variables == {} diff --git a/tests/func/test_packager.py b/tests/func/test_packager.py index fb01451..428106f 100644 --- a/tests/func/test_packager.py +++ b/tests/func/test_packager.py @@ -233,7 +233,8 @@ def test_package_pyapp_cargo_and_move(rye_project, mocker, binary_extensions): @pytest.mark.parametrize("app_entry_type", ut.PYAPP_APP_ENTRY_TYPES) @pytest.mark.parametrize("opt_deps", ["gui", None]) @pytest.mark.parametrize("opt_pyapp_vars", ["PYAPP_SOMETHING 2", None]) -def test_set_env(rye_project, mocker, app_entry_type, opt_deps, opt_pyapp_vars): +@pytest.mark.parametrize("gui", [True, False]) +def test_set_env(rye_project, mocker, app_entry_type, opt_deps, opt_pyapp_vars, gui): """Set environment for `PyApp` packaging.""" config = PyProjectParser() exec_spec = config.app_entry @@ -252,6 +253,7 @@ def test_set_env(rye_project, mocker, app_entry_type, opt_deps, opt_pyapp_vars): tmp_split = opt_pyapp_vars.split() opt_pyapp_vars = {tmp_split[0]: tmp_split[1]} pyproject_writer("optional_pyapp_vars", opt_pyapp_vars) + pyproject_writer("is_gui", gui) packager = PackageApp() packager.build() @@ -267,6 +269,11 @@ def test_set_env(rye_project, mocker, app_entry_type, opt_deps, opt_pyapp_vars): assert os.environ["PYAPP_PIP_OPTIONAL_DEPS"] == opt_deps if opt_pyapp_vars: assert os.environ["PYAPP_SOMETHING"] == "2" + if gui: + assert os.environ["PYAPP_IS_GUI"] == "1" + else: # no such variable should be set! + with pytest.raises(KeyError): + _ = os.environ["PYAPP_IS_GUI"] def test_set_env_delete_existing(rye_project):