Skip to content
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
2 changes: 1 addition & 1 deletion .python-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.14
3.10
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@ doc:
test:
uv run pycodestyle posty tests
uv run flake8 posty tests
uv run mypy posty tests
uv run pytest

test-watch:
find . -name '*py' -or -name 'uv.lock' | entr -r -c make test

# Release-related actions
dist:
uv build
Expand Down
16 changes: 8 additions & 8 deletions posty/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@


@click.group()
def cli():
def cli() -> None:
pass


@cli.command()
def init():
def init() -> None:
"""
Initialize a Posty site
"""
Expand Down Expand Up @@ -45,7 +45,7 @@ def init():
type=click.Path(exists=True),
help='Path to your config file',
)
def build(output, config):
def build(output: str, config: str) -> None:
"""
Build a Posty site as rendered HTML
"""
Expand All @@ -58,7 +58,7 @@ def build(output, config):


@cli.group(name='new')
def _new():
def _new() -> None:
"""
Create a new post or page
"""
Expand All @@ -71,7 +71,7 @@ def _new():
help='Name of the new page',
default='New Page',
)
def page(name):
def page(name: str) -> None:
"""
Create a new page from the template
"""
Expand All @@ -85,7 +85,7 @@ def page(name):
help='Name of the new post',
default='New Post',
)
def post(name):
def post(name: str) -> None:
"""
Create a new page from the template
"""
Expand All @@ -94,7 +94,7 @@ def post(name):


@cli.group(name='import')
def _import():
def _import() -> None:
"""
Import a site from another static site generator
"""
Expand All @@ -103,7 +103,7 @@ def _import():

@_import.command()
@click.argument('path')
def posty1(path):
def posty1(path: str) -> None:
"""
Import a Posty 1.x site from PATH
"""
Expand Down
104 changes: 53 additions & 51 deletions posty/config.py
Original file line number Diff line number Diff line change
@@ -1,81 +1,83 @@
import os.path
import sys
import yaml
from dataclasses import dataclass, field

from .exceptions import InvalidConfig


if sys.version_info >= (3, 3):
from collections.abc import MutableMapping
else:
from collections import MutableMapping
@dataclass
class FeedConfig:
rss: bool = True
atom: bool = True


class Config(MutableMapping):
@dataclass
class CompatConfig:
redirect_posty1_urls: bool = False


@dataclass
class Config:
"""
Config object that gets passed around to various other objects. Loads
config from a given YAML file.

:param path:
Path to a YAML file to read in as config
"""
def __init__(self, path='config.yml'):
self.path = path
self.config = {}

def load(self):
"""
Load the YAML config from the given path, return the config object
"""
if not os.path.exists(self.path):
raise InvalidConfig(
self,
'Unable to read config at {}'.format(self.path)
)

self.config = yaml.safe_load(open(self.path).read())
self.clean_config()
return self

def __len__(self):
return len(self.config)

def __iter__(self):
return iter(self.config)

def __getitem__(self, key):
return self.config[key]
config_path: str

def __setitem__(self, key, value):
self.config[key] = value
author: str
title: str
description: str = ""
base_url: str = "/"

def __delitem__(self, key):
del self.config[key]
num_top_tags: int = 5
num_posts_per_page: int = 5
feeds: FeedConfig = field(default_factory=FeedConfig)
compat: CompatConfig = field(default_factory=CompatConfig)

def clean_config(self):
def __post_init__(self) -> None:
"""
Validate and clean the already-loaded config
"""
c = self.config

if not c.get('author'):
if self.author == "":
raise InvalidConfig(self, 'You must set an author')

if not c.get('title'):
if self.title == "":
raise InvalidConfig(self, 'You must set a title')

c.setdefault('description', '')
c.setdefault('base_url', '/')

if not c['base_url'].endswith('/'):
if not self.base_url.endswith('/'):
raise InvalidConfig(self, 'base_url must end with /')

c.setdefault('num_top_tags', 5)
c.setdefault('num_posts_per_page', 5)
@classmethod
def from_yaml(cls, path: str = 'config.yml') -> "Config":
if not os.path.exists(path):
raise ValueError(
'Unable to read config at {}'.format(path)
)

with open(path) as f:
payload = yaml.safe_load(f)

payload['config_path'] = path

feed_conf = None
if 'feeds' in payload:
feed_conf = FeedConfig(**payload['feeds'])
del payload['feeds']

compat_conf = None
if 'compat' in payload:
compat_conf = CompatConfig(**payload['compat'])
del payload['compat']

new_config = cls(**payload)

c.setdefault('feeds', {})
c['feeds'].setdefault('rss', True)
c['feeds'].setdefault('atom', True)
if feed_conf is not None:
new_config.feeds = feed_conf
if compat_conf is not None:
new_config.compat = compat_conf

c.setdefault('compat', {})
c['compat'].setdefault('redirect_posty1_urls', False)
return new_config
9 changes: 7 additions & 2 deletions posty/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from posty.config import Config


class PostyError(RuntimeError):
pass


class InvalidConfig(PostyError):
def __init__(self, config_obj, reason):
def __init__(self, config_obj: "Config", reason: str) -> None:
msg = 'Invalid config at {}. Reason: {}'.format(
config_obj.path,
config_obj.config_path,
reason
)
super(self.__class__, self).__init__(msg)
Expand Down
23 changes: 12 additions & 11 deletions posty/importers.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from .exceptions import UnableToImport
from .model import ABC
from .site import Site


class Importer(ABC):
Expand All @@ -22,15 +23,15 @@ class Importer(ABC):
:param src_path:
Path to the thing to import
"""
def __init__(self, site, src_path):
def __init__(self, site: Site, src_path: str) -> None:
self.site = site
self.src_path = src_path

@abc.abstractmethod
def run(self):
def run(self) -> None:
raise NotImplementedError

def ensure_directories(self):
def ensure_directories(self) -> None:
for _dir in ('media', 'templates', 'pages', 'posts'):
path = os.path.join(self.site.site_path, _dir)
if not os.path.exists(path):
Expand All @@ -45,21 +46,21 @@ class Posty1Importer(Importer):
"""
Importer to pull from a Posty 1.x site
"""
def run(self):
def run(self) -> None:
self.ensure_directories()

self.import_media()
self.import_templates()
self.import_pages()
self.import_posts()

def import_media(self):
def import_media(self) -> None:
self._copy_files('_media', 'media')

def import_templates(self):
def import_templates(self) -> None:
self._copy_files('_templates', 'templates')

def import_pages(self):
def import_pages(self) -> None:
src_dir = os.path.join(self.src_path, '_pages')
dst_dir = os.path.join(self.site.site_path, 'pages')

Expand All @@ -71,7 +72,7 @@ def import_pages(self):
with open(dst_file, 'w') as fh:
fh.write(new_page)

def import_posts(self):
def import_posts(self) -> None:
src_dir = os.path.join(self.src_path, '_posts')
dst_dir = os.path.join(self.site.site_path, 'posts')

Expand All @@ -83,7 +84,7 @@ def import_posts(self):
with open(dst_file, 'w') as fh:
fh.write(new_post)

def _copy_files(self, src, dst):
def _copy_files(self, src: str, dst: str) -> None:
"""
Copy all the files in ``src_dir`` into ``dst_dir``. Each given dir
should be relative to the source/destination sites
Expand All @@ -109,7 +110,7 @@ def _copy_files(self, src, dst):
print((" Looks like {} isn't a file nor dir, "
"not copying.").format(src_path))

def _convert_page(self, old_page):
def _convert_page(self, old_page: str) -> str:
"""
Converts an old Posty 1.x page into a new-style one. Notably just
throws away any existing `url`
Expand All @@ -128,7 +129,7 @@ def _convert_page(self, old_page):

return new_page

def _convert_post(self, old_post):
def _convert_post(self, old_post: str) -> str:
"""
Converts an old Posty post (a string) into a new-style post with a
blurb and updated metadata. Returns a string containing the three YAML
Expand Down
Loading