Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(logo): Support alt text for logos via BrandLogoResource #29

Merged
merged 3 commits into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 38 additions & 5 deletions docs/pkg/py/logo.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,21 @@ BrandLogo()
Brand Logos

`logo` stores a single brand logo or a set of logos at three different size
points and possibly in different color schemes. Store all logo or image
assets in `images` with meaningful names. Logos can be specified at three
different sizes -- `small`, `medium`, and `large` -- and each can be either
a single logo file or a light/dark variant (`brand_yaml.BrandLightDark`).
points and possibly in different color schemes. Store all of your brand's
logo or image assets in `images` with meaningful names. Logos can be mapped
to three preset sizes -- `small`, `medium`, and `large` -- and each can be
either a single logo file or a light/dark variant
(`brand_yaml.BrandLightDark`).

To attach alternative text to an image, provide the image as a dictionary
including `path` (the image location) and `alt` (the short, alternative
text describing the image).

## Attributes {.doc-section .doc-section-attributes}

images

: [dict](`dict`)\[[str](`str`), [FileLocationLocalOrUrlType](`brand_yaml.file.FileLocationLocalOrUrlType`)\] \| None
: [dict](`dict`)\[[str](`str`), [BrandLogoResource](`brand_yaml.logo.BrandLogoResource`)\] \| None

A dictionary containing any number of logos or brand images. You can
refer to these images by their key name in `small`, `medium` or `large`.
Expand Down Expand Up @@ -109,6 +114,34 @@ logo:
large: pandas
```



###### Complete with Alt Text

```{.yaml filename="_brand.yml"}
logo:
images:
mark:
path: logos/pandas/pandas_mark.svg
alt: pandas logo with blue bars and yellow and pink dots
mark-white: logos/pandas/pandas_mark_white.svg
secondary: logos/pandas/pandas_secondary.svg
secondary-white:
path: logos/pandas/pandas_secondary_white.svg
alt: pandas logo with bars and dots over the word "pandas"
pandas: logos/pandas/pandas.svg
pandas-white: logos/pandas/pandas_white.svg
small: mark
medium:
light:
path: logos/pandas/pandas_secondary.svg
alt: pandas logo with bars and dots over the word "pandas"
dark: secondary-white
large:
path: logos/pandas/pandas.svg
alt: pandas bars and dots to the right of the word "pandas"
```

:::

# BrandLightDark { #brand_yaml.BrandLightDark }
Expand Down
2 changes: 0 additions & 2 deletions docs/pkg/py/typography.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,6 @@ color:
background: '#f7f4f4'

typography:
base:
color: foreground
headings:
color: primary
monospace-inline:
Expand Down
21 changes: 21 additions & 0 deletions examples/brand-logo-full-alt.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
logo:
images:
mark:
path: logos/pandas/pandas_mark.svg
alt: pandas logo with blue bars and yellow and pink dots
mark-white: logos/pandas/pandas_mark_white.svg
secondary: logos/pandas/pandas_secondary.svg
secondary-white:
path: logos/pandas/pandas_secondary_white.svg
alt: pandas logo with bars and dots over the word "pandas"
pandas: logos/pandas/pandas.svg
pandas-white: logos/pandas/pandas_white.svg
small: mark
medium:
light:
path: logos/pandas/pandas_secondary.svg
alt: pandas logo with bars and dots over the word "pandas"
dark: secondary-white
large:
path: logos/pandas/pandas.svg
alt: pandas bars and dots to the right of the word "pandas"
17 changes: 15 additions & 2 deletions pkg-py/src/brand_yaml/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from .base import BrandBase
from .color import BrandColor
from .file import FileLocation, FileLocationLocal, FileLocationUrl
from .logo import BrandLogo
from .logo import BrandLogo, BrandLogoResource
from .meta import BrandMeta
from .typography import BrandTypography

Expand Down Expand Up @@ -45,8 +45,9 @@ class Brand(BrandBase):
validate_assignment=True,
)

# TODO @docs: Document Brand attributes
meta: BrandMeta | None = None
logo: str | BrandLogo | None = None
logo: BrandLogo | BrandLogoResource | None = None
color: BrandColor | None = None
typography: BrandTypography | None = None
defaults: dict[str, Any] | None = None
Expand Down Expand Up @@ -306,6 +307,17 @@ def _set_root_path(self):

return self

@field_validator("logo", mode="before")
@classmethod
def _promote_logo_scalar_to_resource(cls, value: Any):
"""
Take a single path value passed to `brand.logo` and promote it into a
[`brand_yaml.BrandLogoResource`](`brand_yaml.BrandLogoResource`).
"""
if isinstance(value, (str, Path, FileLocation)):
return {"path": value}
return value


@overload
def read_brand_yaml(
Expand Down Expand Up @@ -403,6 +415,7 @@ def read_brand_yaml(path: str | Path, as_data: bool = False) -> Brand | dict:
"BrandColor",
"BrandTypography",
"BrandLightDark",
"BrandLogoResource",
"FileLocation",
"FileLocationLocal",
"FileLocationUrl",
Expand Down
120 changes: 102 additions & 18 deletions pkg-py/src/brand_yaml/logo.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@
from __future__ import annotations

from pathlib import Path
from typing import Annotated, Any, Union
from typing import Annotated, Any, Literal, Union

from pydantic import (
AnyUrl,
ConfigDict,
Discriminator,
Tag,
field_validator,
model_validator,
)

Expand All @@ -22,22 +24,60 @@
from .base import BrandBase
from .file import FileLocation, FileLocationLocalOrUrlType


class BrandLogoResource(BrandBase):
"""A logo resource, a file with optional alternative text"""

model_config = ConfigDict(
str_strip_whitespace=True,
frozen=True,
extra="forbid",
use_attribute_docstrings=True,
)

path: FileLocationLocalOrUrlType
"""The path to the logo resource. This can be a local file or a URL."""

alt: str | None = None
"""Alterative text for the image, used for accessibility."""


def brand_logo_type_discriminator(
x: Any,
) -> Literal["file", "light-dark", "resource"]:
if isinstance(x, dict):
if "path" in x:
return "resource"
if "light" in x or "dark" in x:
return "light-dark"

if isinstance(x, BrandLightDark):
return "light-dark"
if isinstance(x, BrandLogoResource):
return "resource"

raise TypeError(f"{type(x)} is not a valid brand logo type")


BrandLogoImageType = Union[FileLocationLocalOrUrlType, BrandLogoResource]
"""
A logo image file can be either a local or URL file location, or a dictionary
with `path` and `alt`, the path to the file (local or URL) and an associated
alternative text for the logo image to be used for accessibility.
"""


BrandLogoFileType = Annotated[
Union[
Annotated[FileLocationLocalOrUrlType, Tag("file")],
Annotated[
BrandLightDark[FileLocationLocalOrUrlType], Tag("light-dark")
],
Annotated[BrandLogoResource, Tag("resource")],
Annotated[BrandLightDark[BrandLogoResource], Tag("light-dark")],
],
Discriminator(
lambda x: "light-dark"
if isinstance(x, (dict, BrandLightDark))
else "file"
),
Discriminator(brand_logo_type_discriminator),
]
"""
A logo image file can be either a local or URL file location, or a light-dark
variant that includes both a light and dark color scheme.
A logo image file can be either a local or URL file location with optional
alternative text or a light-dark variant that includes both a light and dark
color scheme.
"""


Expand All @@ -46,16 +86,22 @@
{"path": "brand-logo-simple.yml", "name": "Minimal"},
{"path": "brand-logo-light-dark.yml", "name": "Light/Dark Variants"},
{"path": "brand-logo-full.yml", "name": "Complete"},
{"path": "brand-logo-full-alt.yml", "name": "Complete with Alt Text"},
)
class BrandLogo(BrandBase):
"""
Brand Logos

`logo` stores a single brand logo or a set of logos at three different size
points and possibly in different color schemes. Store all logo or image
assets in `images` with meaningful names. Logos can be specified at three
different sizes -- `small`, `medium`, and `large` -- and each can be either
a single logo file or a light/dark variant (`brand_yaml.BrandLightDark`).
points and possibly in different color schemes. Store all of your brand's
logo or image assets in `images` with meaningful names. Logos can be mapped
to three preset sizes -- `small`, `medium`, and `large` -- and each can be
either a single logo file or a light/dark variant
(`brand_yaml.BrandLightDark`).

To attach alternative text to an image, provide the image as a dictionary
including `path` (the image location) and `alt` (the short, alternative
text describing the image).

Attributes
----------
Expand Down Expand Up @@ -87,7 +133,7 @@ class BrandLogo(BrandBase):

model_config = ConfigDict(extra="forbid")

images: dict[str, FileLocationLocalOrUrlType] | None = None
images: dict[str, BrandLogoResource] | None = None
small: BrandLogoFileType | None = None
medium: BrandLogoFileType | None = None
large: BrandLogoFileType | None = None
Expand All @@ -109,9 +155,47 @@ def _resolve_image_values(cls, data: Any):
raise ValueError("images must be a dictionary of file locations")

for key, value in images.items():
if isinstance(value, dict):
# pydantic will handle validation of dict values
continue

if not isinstance(value, (str, FileLocation, Path)):
raise ValueError(f"images[{key}] must be a file location")

defs_replace_recursively(data, defs=images, name="logo")
# Promote bare file locations to BrandLogoResource locations
images[key] = {"path": value}

defs_replace_recursively(data, defs=images, name="logo", exclude="path")

return data

@field_validator("small", "medium", "large", mode="before")
@classmethod
def _promote_bare_files_to_logo_resource(cls, value: Any):
"""
Takes any bare file location references and promotes them to the
structure required for BrandLogoResource.

This results in a more nested but consistent data structure where each
image is always a `BrandLogoResource` instance that's guaranteed to have
a `path` item and optionally may include `alt` text.
"""
if isinstance(value, (str, Path, AnyUrl)):
# Bare strings/paths become BrandLogoResource without `alt`
return {"path": value}

if isinstance(value, dict):
for k in ("light", "dark"):
if k not in value:
continue
value[k] = cls._promote_bare_files_to_logo_resource(value[k])

if isinstance(value, BrandLightDark):
for k in ("light", "dark"):
prop = getattr(value, k)
if prop is not None:
setattr(
value, k, cls._promote_bare_files_to_logo_resource(prop)
)

return value
40 changes: 30 additions & 10 deletions pkg-py/tests/__snapshots__/test_logo/test_brand_logo_ex_full.json
Original file line number Diff line number Diff line change
@@ -1,18 +1,38 @@
{
"logo": {
"images": {
"mark": "logos/pandas/pandas_mark.svg",
"mark-white": "logos/pandas/pandas_mark_white.svg",
"pandas": "logos/pandas/pandas.svg",
"pandas-white": "logos/pandas/pandas_white.svg",
"secondary": "logos/pandas/pandas_secondary.svg",
"secondary-white": "logos/pandas/pandas_secondary_white.svg"
"mark": {
"path": "logos/pandas/pandas_mark.svg"
},
"mark-white": {
"path": "logos/pandas/pandas_mark_white.svg"
},
"pandas": {
"path": "logos/pandas/pandas.svg"
},
"pandas-white": {
"path": "logos/pandas/pandas_white.svg"
},
"secondary": {
"path": "logos/pandas/pandas_secondary.svg"
},
"secondary-white": {
"path": "logos/pandas/pandas_secondary_white.svg"
}
},
"large": {
"path": "logos/pandas/pandas.svg"
},
"large": "logos/pandas/pandas.svg",
"medium": {
"dark": "logos/pandas/pandas_secondary_white.svg",
"light": "logos/pandas/pandas_secondary.svg"
"dark": {
"path": "logos/pandas/pandas_secondary_white.svg"
},
"light": {
"path": "logos/pandas/pandas_secondary.svg"
}
},
"small": "logos/pandas/pandas_mark.svg"
"small": {
"path": "logos/pandas/pandas_mark.svg"
}
}
}
Loading