Skip to content

Commit

Permalink
feature, python: allow for users to define custom exports from __init…
Browse files Browse the repository at this point in the history
…__.py (#3025)

* add in fern cli changes and docstring

* some seed

* run seed

* need to see why tests fail

* need to see why tests fail

* still need to figure out

* fix ete test

* fix snapshots

* plumb environment docs

* fix

* ignore exhaustive:additional_init_exports

* fix

---------

Co-authored-by: dsinghvi <dsinghvi@umich.edu>
  • Loading branch information
armandobelardo and dsinghvi authored Feb 21, 2024
1 parent 953a306 commit bd6f550
Show file tree
Hide file tree
Showing 59 changed files with 2,013 additions and 38 deletions.
39 changes: 39 additions & 0 deletions generators/python/sdk/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,45 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.11.1] - 2024-02-20

- Improvement: Python now supports specifying files to auto-export from the root `__init__.py` file, this means you can export custom classes and functions from your package for users to access like so:

```python
from my_package import custom_function
```

the configuration for this is:

```yaml
# generators.yml
python-sdk:
generators:
- name: fernapi/fern-python-sdk
version: 0.11.1
config:
additional_init_exports:
- from: file_with_custom_function
imports:
- custom_function
```

- Chore: Add a docstring for base clients to explain usage, example:

```python
class SeedTest:
"""
Use this class to access the different functions within the SDK. You can instantiate any number of clients with different configuration that will propogate to these functions.
---
from seed.client import SeedTest
client = SeedTest(
token="YOUR_TOKEN",
base_url="https://yourhost.com/path/to/api",
)
"""
```

## [0.11.0] - 2024-02-19

- Improvement: Python now supports a wider range of types for file upload, mirroring the `httpx` library used under the hood, these are grouped under a new type `File`:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ def __init__(
constructor: ClassConstructor = None,
docstring: Docstring = None,
snippet: Optional[str] = None,
write_parameter_docstring: bool = False,
):
self.name = name
self.extends = list(extends or [])
Expand All @@ -42,6 +43,7 @@ def __init__(
self.class_vars: List[VariableDeclaration] = []
self.statements: List[AstNode] = []
self.ghost_references: OrderedSet[Reference] = OrderedSet()
self.write_parameter_docstring = write_parameter_docstring

def add_class_var(self, variable_declaration: VariableDeclaration) -> None:
self.class_vars.append(variable_declaration)
Expand Down Expand Up @@ -157,18 +159,80 @@ def write(self, writer: NodeWriter) -> None:
writer.write_line(":")

with writer.indent():
if self.docstring is not None:
parameters = (
self.constructor.function_declaration.signature.named_parameters if self.constructor is not None else []
)
if (
self.docstring is not None
or self.snippet is not None
or (len(parameters) > 0 and self.write_parameter_docstring)
):
writer.write_line('"""')

if self.docstring is not None:
writer.write_node(self.docstring)
writer.write_newline_if_last_line_not()
if self.snippet is None and len(parameters) == 0:
writer.write_line('"""')
elif len(parameters) == 0:
writer.write_line("---")

if len(parameters) > 0 and self.write_parameter_docstring:
if self.docstring is not None:
# Include a line between the endpoint docs and field docs.
writer.write_line()
writer.write_line("Parameters:")
with writer.indent():
for i, param in enumerate(parameters):
if i > 0:
writer.write_line()

if param.docs is None:
writer.write(f"- {param.name}: ")
if param.type_hint is not None:
writer.write_node(param.type_hint)
writer.write_line(".")
continue

split = param.docs.split("\n")
if len(split) == 1:
writer.write(f"- {param.name}: ")
if param.type_hint is not None:
writer.write_node(param.type_hint)
writer.write_line(f". {param.docs}")
continue

# Handle multi-line comments at the same level of indentation for the same field,
# e.g.
#
# - userId: str. This is a multi-line comment.
# This one has three lines
# in total.
#
# - request: Request. The request body.
#
indent = ""
for i, line in enumerate(split):
if i == 0:
# Determine the level of indentation we need by capturing the length
# before and after we write the type hint.
writer.write(f"- {param.name}: ")
before = writer.size()
if param.type_hint is not None:
writer.write_node(param.type_hint)
after = writer.size()
writer.write_line(f". {line}")
indent = " " * (len(param.name) + (after - before) + 4)
continue
writer.write(f" {indent} {line}")
if i < len(split) - 1:
writer.write_line()
if self.snippet is None:
writer.write_line('"""')
else:
writer.write_line("---")

if self.snippet is not None:
if self.docstring is None:
writer.write_line('"""')
writer.write(self.snippet)
writer.write_newline_if_last_line_not()
writer.write_line('"""')
Expand Down
11 changes: 11 additions & 0 deletions generators/python/src/fern_python/codegen/module_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,20 @@
from functools import cmp_to_key
from typing import DefaultDict, List, Optional, Sequence, Set, Tuple

import pydantic

from . import AST
from .filepath import ExportStrategy, Filepath
from .writer_impl import WriterImpl

RelativeModulePath = Tuple[str, ...]


class ModuleExport(pydantic.BaseModel):
from_: str = pydantic.Field(alias="from")
imports: List[str]


@dataclass
class ModuleInfo:
exports: DefaultDict[RelativeModulePath, Set[str]]
Expand Down Expand Up @@ -38,6 +45,10 @@ def __init__(self, *, should_format: bool, sorted_modules: Optional[Sequence[str
self._should_format = should_format
self._sorted_modules = sorted_modules or []

def register_additional_exports(self, path: AST.ModulePath, exports: List[ModuleExport]) -> None:
for export in exports:
self._module_infos[path].exports[(export.from_,)].update(export.imports)

def register_exports(self, filepath: Filepath, exports: Set[str]) -> None:
module_being_exported_from: AST.ModulePath = tuple(
directory.module_name for directory in filepath.directories
Expand Down
7 changes: 5 additions & 2 deletions generators/python/src/fern_python/codegen/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@
from dataclasses import dataclass
from pathlib import Path
from types import TracebackType
from typing import Optional, Sequence, Set, Type
from typing import List, Optional, Sequence, Set, Type

from fern_python.codegen import AST
from fern_python.codegen.pyproject_toml import PyProjectToml, PyProjectTomlPackageConfig

from .dependency_manager import DependencyManager
from .filepath import Filepath
from .module_manager import ModuleManager
from .module_manager import ModuleExport, ModuleManager
from .reference_resolver_impl import ReferenceResolverImpl
from .source_file import SourceFile, SourceFileImpl
from .writer_impl import WriterImpl
Expand Down Expand Up @@ -59,6 +59,9 @@ def __init__(
self._should_format_files = should_format_files
self._whitelabel = whitelabel

def add_init_exports(self, path: AST.ModulePath, exports: List[ModuleExport]) -> None:
self._module_manager.register_additional_exports(path, exports)

def add_dependency(self, dependency: AST.Dependency) -> None:
self._dependency_manager.add_dependency(dependency)

Expand Down
Loading

0 comments on commit bd6f550

Please sign in to comment.