Skip to content

Commit

Permalink
get icon from any assets folder (#50)
Browse files Browse the repository at this point in the history
* Fix icon getting, add docs for icons of GUI (Tips)

* disallow icons in hidden folders

* update guide and changelogc
  • Loading branch information
trappitsch authored Jun 7, 2024
1 parent 64e5a8f commit bc08b77
Show file tree
Hide file tree
Showing 9 changed files with 143 additions and 48 deletions.
3 changes: 3 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
- Allow icons to be stored in any folder that is named `assets`.
- Finish installers for MacOS.

## v0.2.0

- Released binary is now named after the project name, not after the python package name
Expand Down
2 changes: 1 addition & 1 deletion docs/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ in the `target/release` folder.
### GUIs

In order to package a GUI,
you should have an icon file in the `assets` folder in your project.
you should have an icon file in an `assets` folder in your project.
For Linux, the icon file should be a `svg`, `png`, `jpg`, or `jpeg` file.
For Windows, the icon file should be a `ico` file.
In either case, the icon file(s) must be named `icon.<ext>`,
Expand Down
67 changes: 67 additions & 0 deletions docs/tips.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Tips and Tricks for using `box`

## GUI programs and icons

### Icon files

With GUI programs, you will want to provide an icon file.
This icon file should be in a directory named `assets`.
The location of the `assets` directory is not important, but must be inside the project.
The icon itself must be named `icon.EXT`, where `EXT` is the file extension of the icon file.

Different icon files are required for different platforms.

- **Linux**: The icon file should be a `.svg` or `.png` file.
- **Windows**: The icon file should be a `.ico` file. There are many online converters that work reasonably well to turn `.svg` or `.png` files into `.ico` files.
- **MacOS**: The icon file should be a `.icns` file. If you create these on Linux, you might want to check [this article](https://dentrassi.de/2014/02/25/creating-mac-os-x-icons-icns-on-linux/) on how to create them.

### Associating your icon with your GUIk

THe installer itself will only associate the icon with the executable but NOT with your GUI.
Basically, the executable is a python shell that will run your GUI program and then detach from it.
Thus, the actual GUI program and the executable that your user will get are different.

!!! tip

You should point this out to your user, since it can be confusing.
For example, start menu / dock pinning on Windows and MacOS will only work with the executable,
but not with the GUI program itself.
This will be slightly confusing when executing the program, since the program will not be associated with
the pinned start menu.
If you have an idea how to fix this, please let me know!

If you are using `PyQt` or `PySide`,
you can associate the icon with the GUI program in the Main Window class.
Let's assume the following file tree:

```plaintext
|- src
| |- pkg_name
| | |- __init__.py
| | |- main.py
| | |- assets
| | | |- __init__.py
| | | |- icon.svg
```

Here, we have the `assets` folder along with the source,
we put an `__init__`.py in there in order to ensure that it's added to the package.
In the `main.py` file, you can then associate the icon with the GUI program like this:

```python
from pathlib import Path

from qtpy import QtWidgets, QtGui

class MyProgram(QtWidgets.QMainWindow):
"""Main window of your program."""

def __init__(self):
super().__init__()

icon = Path(__file__).parent.joinpath("assets/icon.svg").absolute()
self.setWindowIcon(QtGui.QIcon(str(icon)))
```

This will associate the icon with the GUI program,
and the icon will be shown in the window title bar and in the task bar.
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,4 @@ nav:
- Usage guide: guide.md
- Changelog: changelog.md
- Examples: examples.md
- Tips: tips.md
1 change: 0 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ dev-dependencies = [
"pytest-mock>=3.12.0",
"gitpython>=3.1.42",
"build>=1.2.1",
"applecrate>=0.2.0",
]

[tool.rye.scripts]
Expand Down
18 changes: 0 additions & 18 deletions requirements-dev.lock
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@
# generate-hashes: false

-e file:.
applecrate==0.2.0
# via box-packager
babel==2.14.0
# via mkdocs-material
bracex==2.4
Expand All @@ -21,7 +19,6 @@ certifi==2024.2.2
charset-normalizer==3.3.2
# via requests
click==8.1.7
# via applecrate
# via box-packager
# via mkdocs
# via mkdocs-click
Expand All @@ -31,10 +28,6 @@ colorama==0.4.6
# via mkdocs-material
coverage==7.4.3
# via pytest-cov
dmgbuild==1.6.1
# via box-packager
ds-store==1.3.1
# via dmgbuild
ghp-import==2.1.0
# via mkdocs
gitdb==4.0.11
Expand All @@ -45,21 +38,15 @@ idna==3.6
iniconfig==2.0.0
# via pytest
jinja2==3.1.3
# via applecrate
# via mkdocs
# via mkdocs-material
mac-alias==2.2.2
# via dmgbuild
# via ds-store
markdown==3.5.2
# via mkdocs
# via mkdocs-click
# via mkdocs-material
# via pymdown-extensions
markdown-it-py==3.0.0
# via rich
markdown2==2.4.13
# via applecrate
markupsafe==2.1.5
# via jinja2
# via mkdocs
Expand All @@ -80,16 +67,13 @@ mkdocs-material==9.5.13
mkdocs-material-extensions==1.3.1
# via mkdocs-material
packaging==23.2
# via applecrate
# via build
# via mkdocs
# via pytest
paginate==0.5.6
# via mkdocs-material
pathspec==0.12.1
# via mkdocs
pip==24.0
# via applecrate
platformdirs==4.2.0
# via mkdocs
pluggy==1.4.0
Expand Down Expand Up @@ -128,8 +112,6 @@ six==1.16.0
# via python-dateutil
smmap==5.0.1
# via gitdb
toml==0.10.2
# via applecrate
tomlkit==0.12.4
# via box-packager
typing-extensions==4.10.0
Expand Down
18 changes: 0 additions & 18 deletions requirements.lock
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@
# generate-hashes: false

-e file:.
applecrate==0.2.0
# via box-packager
babel==2.14.0
# via mkdocs-material
bracex==2.4
Expand All @@ -20,38 +18,27 @@ certifi==2024.2.2
charset-normalizer==3.3.2
# via requests
click==8.1.7
# via applecrate
# via box-packager
# via mkdocs
# via mkdocs-click
# via rich-click
colorama==0.4.6
# via box-packager
# via mkdocs-material
dmgbuild==1.6.1
# via box-packager
ds-store==1.3.1
# via dmgbuild
ghp-import==2.1.0
# via mkdocs
idna==3.6
# via requests
jinja2==3.1.3
# via applecrate
# via mkdocs
# via mkdocs-material
mac-alias==2.2.2
# via dmgbuild
# via ds-store
markdown==3.5.2
# via mkdocs
# via mkdocs-click
# via mkdocs-material
# via pymdown-extensions
markdown-it-py==3.0.0
# via rich
markdown2==2.4.13
# via applecrate
markupsafe==2.1.5
# via jinja2
# via mkdocs
Expand All @@ -72,14 +59,11 @@ mkdocs-material==9.5.13
mkdocs-material-extensions==1.3.1
# via mkdocs-material
packaging==23.2
# via applecrate
# via mkdocs
paginate==0.5.6
# via mkdocs-material
pathspec==0.12.1
# via mkdocs
pip==24.0
# via applecrate
platformdirs==4.2.0
# via mkdocs
pygments==2.17.2
Expand All @@ -106,8 +90,6 @@ rich-click==1.7.3
# via box-packager
six==1.16.0
# via python-dateutil
toml==0.10.2
# via applecrate
tomlkit==0.12.4
# via box-packager
typing-extensions==4.10.0
Expand Down
37 changes: 27 additions & 10 deletions src/box/installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,33 +293,50 @@ def _check_release(self) -> Path:
def get_icon(suffix: str = None) -> Path:
"""Return the icon file path.
Walks through all the assets folders and returns the first `icon.suffix` file found.
If no suffix is provided, the following priorites will be returned (depending
on file availability):
- icon.svg
- icon.png
- icon.jpg
- icon.jpeg
Note: Windows `.ico` files must be called out explicitly, same with MacOS `.icns` files.
Note: Windows `.ico` files must be called out explicitly,
same with MacOS `.icns` files.
:param suffix: The suffix of the icon file.
:return: The path to the icon file.
:raises ClickException: If no icon file is found.
"""
icon_file = Path.cwd().joinpath("assets/icon")

excluded_folders = ["build", "dist", "target", "venv"]
suffixes = ["svg", "png", "jpg", "jpeg"]
if suffix:
suffixes = [suffix] # overwrite exisiting and only check this.

for suffix in suffixes:
icon_file = icon_file.with_suffix(f".{suffix}")
if icon_file.exists():
return icon_file
for root, dirs, _ in os.walk("."):
# excluded folders
root_fld = root.split("/")
if len(root_fld) > 1 and (
root_fld[1] in excluded_folders or root_fld[1].startswith(".")
):
continue

for dir in dirs:
if dir != "assets":
continue

icon_file = Path.cwd().joinpath(f"{root}/{dir}/icon")

if suffix:
suffixes = [suffix] # overwrite exisiting and only check this.

for suffix in suffixes:
icon_file = icon_file.with_suffix(f".{suffix}")
if icon_file.exists():
return icon_file

raise click.ClickException(
f"No icon file found. Please provide an icon file. "
f"No icon file found. Please provide an icon file in an `assets` folder. "
f"Valid formats are {', '.join(suffixes)}."
)
44 changes: 44 additions & 0 deletions tests/unit/test_installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@ def test_get_icon(suffix, tmp_path_chdir):
assert isinstance(inst.get_icon(), Path)


@pytest.mark.parametrize("suffix", ["svg", "png", "jpg", "jpeg"])
def test_get_icon_subfolder(suffix, tmp_path_chdir):
"""Get an icon file for various suffixes from src/package/assets folder."""
fldr = Path.cwd().joinpath("src").joinpath("package")
fldr.mkdir(parents=True)
create_icon(suffix, fldr)

assert isinstance(inst.get_icon(), Path)


def test_get_specific_icon(tmp_path_chdir):
"""Get an `.ico` file back from the get_icon routine."""
create_icon("ico", Path.cwd())
Expand All @@ -54,3 +64,37 @@ def test_get_icon_no_icon(tmp_path_chdir):
"""Raise an exception if no icon file is found."""
with pytest.raises(click.ClickException):
inst.get_icon()


def test_get_icon_no_icon_in_subfolders(tmp_path_chdir):
"""Raise an exception if no icon file is found and skip excluded folders."""
# put some icons into folders that must be excluded
dist = Path.cwd().joinpath("dist")
dist.mkdir()
create_icon("svg", dist)
build = Path.cwd().joinpath("build")
build.mkdir()
create_icon("svg", build)
target = Path.cwd().joinpath("target")
target.mkdir()
create_icon("svg", target)
venv = Path.cwd().joinpath("venv")
venv.mkdir()
create_icon("svg", venv)
hidden_src = Path.cwd().joinpath(".src")
hidden_src.mkdir()
create_icon("svg", hidden_src)

with pytest.raises(click.ClickException):
inst.get_icon()


def test_get_icon_icon_in_wrong_folder(tmp_path_chdir):
"""Raise an exception icon file cannot be found in assets folder."""
# put some icons into folders that must be excluded
fldr = Path.cwd().joinpath("src")
fldr.mkdir()
create_icon("ico", fldr)

with pytest.raises(click.ClickException):
inst.get_icon()

0 comments on commit bc08b77

Please sign in to comment.