Skip to content

Commit

Permalink
Merge pull request #52 from mdrachuk/lightweight-async
Browse files Browse the repository at this point in the history
Remove Content from Site parents
  • Loading branch information
mdrachuk committed Jan 21, 2020
2 parents a1649d8 + a824020 commit 1c5adf3
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 48 deletions.
2 changes: 1 addition & 1 deletion lightweight/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,4 @@ def blog_posts(source):
logger = logging.getLogger('lightweight')
logger.setLevel(logging.INFO)

__version__ = '1.0.0.dev41'
__version__ = '1.0.0.dev42'
5 changes: 0 additions & 5 deletions lightweight/files.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import asyncio
import os
from contextlib import contextmanager
from glob import glob
Expand Down Expand Up @@ -37,9 +36,5 @@ def directory(location: Union[str, Path]):
"""
cwd = os.getcwd()
os.chdir(str(location))
prev_loop = asyncio.get_event_loop()
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
yield
os.chdir(cwd)
asyncio.set_event_loop(prev_loop)
156 changes: 116 additions & 40 deletions lightweight/site.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,27 @@

import asyncio
import os
from abc import abstractmethod, ABC
from asyncio import gather
from collections import defaultdict
from dataclasses import dataclass, replace
from datetime import datetime
from itertools import chain
from os import getcwd
from pathlib import Path
from shutil import rmtree
from typing import overload, Union, Optional, Collection, Iterator, List, Set, Dict
from typing import overload, Union, Optional, Collection, List, Set, Dict
from urllib.parse import urlparse, urljoin

from lightweight.content.content import Content
from lightweight.content.copies import copy
from lightweight.empty import Empty, empty
from lightweight.errors import AbsolutePathIncluded, IncludedDuplicate
from lightweight.files import paths, directory
from lightweight.generation import GenContext, GenPath, GenTask, schedule
from lightweight.generation import GenContext, GenTask, schedule


class Site(Content):
class Site:
"""A static site for generation, which is basically a collection of [Content].
Site is one of the few mutable Lightweight components. It is available to content during [write][Content.write],
Expand All @@ -47,7 +49,7 @@ class Site(Content):
```
"""
url: str
content: List[IncludedContent]
content: Includes
title: Optional[str]
icon_url: Optional[str]
logo_url: Optional[str]
Expand All @@ -61,7 +63,7 @@ def __init__(
self,
url: str,
*,
content: Collection[IncludedContent] = None,
content: Includes = None,
title: Optional[str] = None,
icon_url: Optional[str] = None,
logo_url: Optional[str] = None,
Expand All @@ -78,7 +80,7 @@ def __init__(
if not url.endswith('/'):
raise ValueError(f'Site URL ({url}) must end with a forward slash (/).')
self.url = url
self.content = [] if not content else list(content)
self.content = Includes() if not content else content
self.title = title
self.icon_url = icon_url
self.logo_url = logo_url
Expand All @@ -96,7 +98,7 @@ def __init__(
def copy(
self,
url: Union[str, Empty] = empty,
content: Union[Collection[IncludedContent], Empty] = empty,
content: Union[Includes, Empty] = empty,
title: Union[Optional[str], Empty] = empty,
icon_url: Union[Optional[str], Empty] = empty,
description: Union[Optional[str], Empty] = empty,
Expand Down Expand Up @@ -131,11 +133,15 @@ def include(self, location: str):
def include(self, location: str, content: Content):
"""Include the content at the provided location."""

@overload
def include(self, location: str, content: Site):
"""Include the content at the provided location."""

@overload
def include(self, location: str, content: str):
"""Copy files from content to location."""

def include(self, location: str, content: Union[Content, str, None] = None):
def include(self, location: str, content: Union[Content, Site, str, None] = None):
"""Included the content at the location.
Note the content write is executed only upon calling [`Site.generate()`][Site.generate].
Expand All @@ -153,28 +159,42 @@ def include(self, location: str, content: Union[Content, str, None] = None):
contents = {str(path): copy(path) for path in paths(location)}
if not len(contents):
raise FileNotFoundError(f'There were no files at paths: {location}')
[self._include(path, content_, cwd) for path, content_ in contents.items()]
[self._include_content(path, content_, cwd) for path, content_ in contents.items()]
elif isinstance(content, Content):
self._include(location, content, cwd)
self._include_content(location, content, cwd)
elif isinstance(content, Site):
self._include_site(location, content, cwd)
elif isinstance(content, str):
source = Path(content)
if not source.exists():
raise FileNotFoundError(f'File does not exist: {content}')
self._include(location, copy(source), cwd)
self._include_content(location, copy(source), cwd)
else:
raise ValueError('Content, str, or None types are accepted as include parameter')

def _include(self, location: str, content: Content, cwd: str):
if location in self:
raise IncludedDuplicate(at=location)
self.content.append(
def _include_content(self, location: str, content: Content, cwd: str):
self._include(
IncludedContent(
location=location,
content=content,
cwd=cwd
)
)

def _include_site(self, location: str, site: Site, cwd: str):
self._include(
IncludedSite(
location=location,
site=site,
cwd=cwd
)
)

def _include(self, c: Included):
if c.location in self.content:
raise IncludedDuplicate(at=c.location)
self.content.add(c)

def generate(self, out: Union[str, Path] = 'out'):
"""Generate the site in directory provided as out.
Expand All @@ -188,20 +208,15 @@ def generate(self, out: Union[str, Path] = 'out'):
out.mkdir(parents=True, exist_ok=True)
self._generate(out)

def write(self, path: GenPath, ctx: GenContext):
"""Write the current site at path.
Executed when this site is part of the other site."""
self._generate((ctx.out / path.relative_path).absolute())

def _generate(self, out: Path):
ctx = GenContext(out=out, site=self)
tasks = defaultdict(list) # type: Dict[str, List[GenTask]]
all_tasks = list() # type: List[GenTask]
for ic in self.content:
task = GenTask(ctx.path(ic.location), ic.content, ic.cwd)
tasks[ic.cwd].append(task)
all_tasks.append(task)
_tasks = ic.make_tasks(ctx)
for task in _tasks:
tasks[task.cwd].append(task)
all_tasks.extend(_tasks)
ctx.tasks = tuple(all_tasks) # injecting tasks, for other content to have access to site structure

loop = asyncio.new_event_loop()
Expand Down Expand Up @@ -248,34 +263,67 @@ def __getitem__(self, location: str) -> Site:
assert <static> not in posts
```
"""
content = self.content_at_path(Path(location))
content = self.content.at_path(Path(location))
if not content:
raise KeyError(f'There is no content at path "{location}"')
return self.copy(content=content, url=self / location + '/')

def __iter__(self) -> Iterator[IncludedContent]:
"""Iterate over the site’s included content."""
return iter(self.content)

def content_at_path(self, target: Path) -> Collection[IncludedContent]:
target = Path(target)
return [
replace(ic, location=clip_path_parts(len(target.parts), ic.path))
for ic in self.content
if all(actual == expected for actual, expected in zip(ic.path.parts, target.parts))
]
def clip_path_parts(number: int, path: Path) -> str:
return os.path.join(*path.parts[number:])


class Includes:
ics: List[Included]
by_cwd: Dict[str, List[Included]]
by_location: Dict[str, Included]

def __init__(self):
self.ics = []
self.by_cwd = defaultdict(list)
self.by_location = {}

def add(self, ic: Included):
self.ics.append(ic)
self.by_cwd[ic.cwd].append(ic)
self.by_location[ic.location] = ic

def __contains__(self, location: str) -> bool:
return location in map(lambda c: c.location, self.content)
return location in self.by_location

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

def clip_path_parts(number: int, path: Path) -> str:
return os.path.join(*path.parts[number:])
def __getitem__(self, location: str):
return self.by_location[location]

def at_path(self, target: Path) -> Includes:
target = Path(target)
cc = Includes()
for ic in self.ics:
if all(actual == expected for actual, expected in zip(ic.path.parts, target.parts)):
new_loc = clip_path_parts(len(target.parts), ic.path)
new_ic = replace(ic, location=new_loc)
cc.add(new_ic)
return cc


class Included(ABC):
location: str
cwd: str

@property
def path(self):
return Path(self.location)

@abstractmethod
def make_tasks(self, ctx: GenContext) -> List[GenTask]:
""""""


@dataclass(frozen=True)
class IncludedContent:
"""The [content][Content] included by [Site].
class IncludedContent(Included):
"""The [content][Content] included by a [Site].
Contains the site’s location and `cwd` (current working directory) of the content.
Expand All @@ -292,6 +340,34 @@ class IncludedContent:
def path(self):
return Path(self.location)

def make_tasks(self, ctx: GenContext) -> List[GenTask]:
return [GenTask(ctx.path(self.location), self.content, self.cwd)]


@dataclass(frozen=True)
class IncludedSite(Included):
"""Another site included by a [Site].
Contains the relative location and `cwd` (current working directory) of the content.
Location is a string with an output path relative to generation out directory.
It does not include a leading forward slash.
`cwd` is important for properly generating subsites.
"""
location: str
site: Site
cwd: str

@property
def path(self):
return Path(self.location)

def make_tasks(self, ctx: GenContext) -> List[GenTask]:
list_of_lists = [ic.make_tasks(ctx) for ic in self.site.content.ics]
tasks = list(chain.from_iterable(list_of_lists))
return [replace(task, path=ctx.path(self.path / task.path.relative_path)) for task in tasks]


@dataclass(frozen=True)
class Author:
Expand Down
2 changes: 1 addition & 1 deletion tests/test_other.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,4 @@ def test_gen_path_location(tmp_path: Path):

def test_jinja_env_does_not_allow_undefined():
with pytest.raises(UndefinedError):
jinja_env.from_string('{{something}}').render()
jinja_env.from_string('{{something}}').render()
12 changes: 11 additions & 1 deletion tests/test_site_nesting.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from pathlib import Path

from lightweight import Site
from lightweight import Site, directory
from lightweight.content.copies import FileCopy


Expand All @@ -20,3 +20,13 @@ def test_include_site(tmp_path: Path):
root.generate(tmp_path)
assert (test_out / 'child/test.html').exists()
assert (test_out / 'child/test.html').read_text() == src_content


def test_subsite_cwd_change(tmp_path: Path):
site = Site('https://example.org/')
site.include('index.html', 'site/index.html')
with directory('site'):
subsite = Site('https://example.org/')
subsite.include('file')
site.include('subsite', subsite)
site.generate(out=tmp_path)

0 comments on commit 1c5adf3

Please sign in to comment.