Skip to content

Commit

Permalink
Merge pull request #169 from hculea/hculea/add-python-support-in-type…
Browse files Browse the repository at this point in the history
…share

Add Typeshare support for Python
  • Loading branch information
kareid authored Dec 9, 2024
2 parents c9d270a + aa12f2b commit b6e6bf1
Show file tree
Hide file tree
Showing 55 changed files with 2,133 additions and 286 deletions.
418 changes: 182 additions & 236 deletions Cargo.lock

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ _And in the darkness, compile them_ 💍


Do you like manually managing types that need to be passed through an FFI layer, so that your code doesn't archaically break at runtime? Be honest, nobody does. Typeshare is here to take that burden away from you! Leveraging the power of the `serde` library, Typeshare is a tool that converts your
Rust types into their equivalent forms in Swift, Go**, Kotlin, Scala and Typescript, keeping
Rust types into their equivalent forms in Swift, Go**, Python**, Kotlin, Scala and Typescript, keeping
your cross-language codebase in sync. With automatic implementation for serialization and deserialization on both sides of the FFI, Typeshare does all the heavy lifting for you. It can even handle generics and convert effortlessly between standard libraries in different languages!

**A few caveats. See [here](#a-quick-refresher-on-supported-languages) for more details.
Expand Down Expand Up @@ -98,12 +98,14 @@ Are you getting weird deserialization issues? Did our procedural macro throw a c
- Swift
- Typescript
- Go**
- Python** (see list of limitations [here](https://github.com/1Password/typeshare/issues/217))


If there is a language that you want Typeshare to generate definitions for, you can either:
1. Open an issue in this repository requesting your language of choice.
2. Implement support for that language and open a PR with your implementation. We would be eternally grateful! 🙏

** Right now, Go support is experimental. Enable the `go` feature when installing typeshare-cli if you want to use it.
** Right now, Go and Python support is experimental. Enable the `go` or `python` features, respectively, when installing typeshare-cli if you want to use these.

## Credits

Expand Down
1 change: 1 addition & 0 deletions cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ path = "src/main.rs"

[features]
go = []
python = []

[dependencies]
clap = { version = "4.5", features = [
Expand Down
6 changes: 5 additions & 1 deletion cli/data/tests/mappings_config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,8 @@
"DateTime" = "String"

[go.type_mappings]
"DateTime" = "string"
"DateTime" = "string"

[python.type_mappings]
"DateTime" = "datetime"
"Url" = "AnyUrl"
2 changes: 2 additions & 0 deletions cli/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ pub enum AvailableLanguage {
Typescript,
#[cfg(feature = "go")]
Go,
#[cfg(feature = "python")]
Python,
}

#[derive(clap::Parser)]
Expand Down
14 changes: 14 additions & 0 deletions cli/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ use std::{

const DEFAULT_CONFIG_FILE_NAME: &str = "typeshare.toml";

#[derive(Default, Serialize, Deserialize, PartialEq, Eq, Debug)]
#[serde(default)]
#[cfg(feature = "python")]
pub struct PythonParams {
pub type_mappings: HashMap<String, String>,
}

#[derive(Default, Serialize, Deserialize, Debug, PartialEq, Eq)]
#[serde(default)]
pub struct KotlinParams {
Expand Down Expand Up @@ -64,6 +71,8 @@ pub(crate) struct Config {
pub typescript: TypeScriptParams,
pub kotlin: KotlinParams,
pub scala: ScalaParams,
#[cfg(feature = "python")]
pub python: PythonParams,
#[cfg(feature = "go")]
pub go: GoParams,
#[serde(skip)]
Expand Down Expand Up @@ -160,6 +169,11 @@ mod test {
assert_eq!(config.kotlin.type_mappings["DateTime"], "String");
assert_eq!(config.scala.type_mappings["DateTime"], "String");
assert_eq!(config.typescript.type_mappings["DateTime"], "string");
#[cfg(feature = "python")]
{
assert_eq!(config.python.type_mappings["Url"], "AnyUrl");
assert_eq!(config.python.type_mappings["DateTime"], "datetime");
}
#[cfg(feature = "go")]
assert_eq!(config.go.type_mappings["DateTime"], "string");
}
Expand Down
19 changes: 15 additions & 4 deletions cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ use flexi_logger::AdaptiveFormat;
use ignore::{overrides::OverrideBuilder, types::TypesBuilder, WalkBuilder};
use log::{error, info};
use parse::parallel_parse;
use typeshare_core::language::GenericConstraints;
#[cfg(feature = "go")]
use typeshare_core::language::Go;
#[cfg(feature = "python")]
use typeshare_core::language::Python;
use typeshare_core::{
context::ParseContext,
language::{
CrateName, GenericConstraints, Kotlin, Language, Scala, SupportedLanguage, Swift,
TypeScript,
},
language::{CrateName, Kotlin, Language, Scala, SupportedLanguage, Swift, TypeScript},
parser::ParsedData,
reconcile::reconcile_aliases,
};
Expand Down Expand Up @@ -93,6 +93,8 @@ fn generate_types(config_file: Option<&Path>, options: &Args) -> anyhow::Result<
args::AvailableLanguage::Typescript => SupportedLanguage::TypeScript,
#[cfg(feature = "go")]
args::AvailableLanguage::Go => SupportedLanguage::Go,
#[cfg(feature = "python")]
args::AvailableLanguage::Python => SupportedLanguage::Python,
},
};

Expand Down Expand Up @@ -222,6 +224,15 @@ fn language(
SupportedLanguage::Go => {
panic!("go support is currently experimental and must be enabled as a feature flag for typeshare-cli")
}
#[cfg(feature = "python")]
SupportedLanguage::Python => Box::new(Python {
type_mappings: config.python.type_mappings,
..Default::default()
}),
#[cfg(not(feature = "python"))]
SupportedLanguage::Python => {
panic!("python support is currently experimental and must be enabled as a feature flag for typeshare-cli")
}
}
}

Expand Down
1 change: 1 addition & 0 deletions cli/src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ fn output_file_name(language_type: SupportedLanguage, crate_name: &CrateName) ->
SupportedLanguage::Scala => snake_case(),
SupportedLanguage::Swift => pascal_case(),
SupportedLanguage::TypeScript => snake_case(),
SupportedLanguage::Python => snake_case(),
}
}

Expand Down
2 changes: 2 additions & 0 deletions core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ thiserror = "1"
itertools = "0.12"
lazy_format = "2"
joinery = "2"
topological-sort = { version = "0.2.2"}
convert_case = { version = "0.6.0"}
log.workspace = true
flexi_logger.workspace = true

Expand Down
51 changes: 51 additions & 0 deletions core/data/tests/anonymous_struct_with_rename/output.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from __future__ import annotations

from enum import Enum
from pydantic import BaseModel, ConfigDict, Field
from typing import List, Literal, Union


class AnonymousStructWithRenameListInner(BaseModel):
"""
Generated type representing the anonymous struct variant `List` of the `AnonymousStructWithRename` Rust enum
"""
list: List[str]

class AnonymousStructWithRenameLongFieldNamesInner(BaseModel):
"""
Generated type representing the anonymous struct variant `LongFieldNames` of the `AnonymousStructWithRename` Rust enum
"""
model_config = ConfigDict(populate_by_name=True)

some_long_field_name: str
and_: bool = Field(alias="and")
but_one_more: List[str]

class AnonymousStructWithRenameKebabCaseInner(BaseModel):
"""
Generated type representing the anonymous struct variant `KebabCase` of the `AnonymousStructWithRename` Rust enum
"""
model_config = ConfigDict(populate_by_name=True)

another_list: List[str] = Field(alias="another-list")
camel_case_string_field: str = Field(alias="camelCaseStringField")
something_else: bool = Field(alias="something-else")

class AnonymousStructWithRenameTypes(str, Enum):
LIST = "list"
LONG_FIELD_NAMES = "longFieldNames"
KEBAB_CASE = "kebabCase"

class AnonymousStructWithRenameList(BaseModel):
type: Literal[AnonymousStructWithRenameTypes.LIST] = AnonymousStructWithRenameTypes.LIST
content: AnonymousStructWithRenameListInner

class AnonymousStructWithRenameLongFieldNames(BaseModel):
type: Literal[AnonymousStructWithRenameTypes.LONG_FIELD_NAMES] = AnonymousStructWithRenameTypes.LONG_FIELD_NAMES
content: AnonymousStructWithRenameLongFieldNamesInner

class AnonymousStructWithRenameKebabCase(BaseModel):
type: Literal[AnonymousStructWithRenameTypes.KEBAB_CASE] = AnonymousStructWithRenameTypes.KEBAB_CASE
content: AnonymousStructWithRenameKebabCaseInner

AnonymousStructWithRename = Union[AnonymousStructWithRenameList, AnonymousStructWithRenameLongFieldNames, AnonymousStructWithRenameKebabCase]
43 changes: 43 additions & 0 deletions core/data/tests/can_apply_prefix_correctly/output.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from __future__ import annotations

from enum import Enum
from pydantic import BaseModel
from typing import Dict, List, Literal, Union


class ItemDetailsFieldValue(BaseModel):
hello: str

class AdvancedColorsTypes(str, Enum):
STRING = "String"
NUMBER = "Number"
NUMBER_ARRAY = "NumberArray"
REALLY_COOL_TYPE = "ReallyCoolType"
ARRAY_REALLY_COOL_TYPE = "ArrayReallyCoolType"
DICTIONARY_REALLY_COOL_TYPE = "DictionaryReallyCoolType"

class AdvancedColorsString(BaseModel):
t: Literal[AdvancedColorsTypes.STRING] = AdvancedColorsTypes.STRING
c: str

class AdvancedColorsNumber(BaseModel):
t: Literal[AdvancedColorsTypes.NUMBER] = AdvancedColorsTypes.NUMBER
c: int

class AdvancedColorsNumberArray(BaseModel):
t: Literal[AdvancedColorsTypes.NUMBER_ARRAY] = AdvancedColorsTypes.NUMBER_ARRAY
c: List[int]

class AdvancedColorsReallyCoolType(BaseModel):
t: Literal[AdvancedColorsTypes.REALLY_COOL_TYPE] = AdvancedColorsTypes.REALLY_COOL_TYPE
c: ItemDetailsFieldValue

class AdvancedColorsArrayReallyCoolType(BaseModel):
t: Literal[AdvancedColorsTypes.ARRAY_REALLY_COOL_TYPE] = AdvancedColorsTypes.ARRAY_REALLY_COOL_TYPE
c: List[ItemDetailsFieldValue]

class AdvancedColorsDictionaryReallyCoolType(BaseModel):
t: Literal[AdvancedColorsTypes.DICTIONARY_REALLY_COOL_TYPE] = AdvancedColorsTypes.DICTIONARY_REALLY_COOL_TYPE
c: Dict[str, ItemDetailsFieldValue]

AdvancedColors = Union[AdvancedColorsString, AdvancedColorsNumber, AdvancedColorsNumberArray, AdvancedColorsReallyCoolType, AdvancedColorsArrayReallyCoolType, AdvancedColorsDictionaryReallyCoolType]
76 changes: 76 additions & 0 deletions core/data/tests/can_generate_algebraic_enum/output.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
from __future__ import annotations

from enum import Enum
from pydantic import BaseModel
from typing import List, Literal, Union


class ItemDetailsFieldValue(BaseModel):
"""
Struct comment
"""
pass
class AdvancedColorsTypes(str, Enum):
STRING = "String"
NUMBER = "Number"
UNSIGNED_NUMBER = "UnsignedNumber"
NUMBER_ARRAY = "NumberArray"
REALLY_COOL_TYPE = "ReallyCoolType"

class AdvancedColorsString(BaseModel):
"""
This is a case comment
"""
type: Literal[AdvancedColorsTypes.STRING] = AdvancedColorsTypes.STRING
content: str

class AdvancedColorsNumber(BaseModel):
type: Literal[AdvancedColorsTypes.NUMBER] = AdvancedColorsTypes.NUMBER
content: int

class AdvancedColorsUnsignedNumber(BaseModel):
type: Literal[AdvancedColorsTypes.UNSIGNED_NUMBER] = AdvancedColorsTypes.UNSIGNED_NUMBER
content: int

class AdvancedColorsNumberArray(BaseModel):
type: Literal[AdvancedColorsTypes.NUMBER_ARRAY] = AdvancedColorsTypes.NUMBER_ARRAY
content: List[int]

class AdvancedColorsReallyCoolType(BaseModel):
"""
Comment on the last element
"""
type: Literal[AdvancedColorsTypes.REALLY_COOL_TYPE] = AdvancedColorsTypes.REALLY_COOL_TYPE
content: ItemDetailsFieldValue

# Enum comment
AdvancedColors = Union[AdvancedColorsString, AdvancedColorsNumber, AdvancedColorsUnsignedNumber, AdvancedColorsNumberArray, AdvancedColorsReallyCoolType]
class AdvancedColors2Types(str, Enum):
STRING = "string"
NUMBER = "number"
NUMBER_ARRAY = "number-array"
REALLY_COOL_TYPE = "really-cool-type"

class AdvancedColors2String(BaseModel):
"""
This is a case comment
"""
type: Literal[AdvancedColors2Types.STRING] = AdvancedColors2Types.STRING
content: str

class AdvancedColors2Number(BaseModel):
type: Literal[AdvancedColors2Types.NUMBER] = AdvancedColors2Types.NUMBER
content: int

class AdvancedColors2NumberArray(BaseModel):
type: Literal[AdvancedColors2Types.NUMBER_ARRAY] = AdvancedColors2Types.NUMBER_ARRAY
content: List[int]

class AdvancedColors2ReallyCoolType(BaseModel):
"""
Comment on the last element
"""
type: Literal[AdvancedColors2Types.REALLY_COOL_TYPE] = AdvancedColors2Types.REALLY_COOL_TYPE
content: ItemDetailsFieldValue

AdvancedColors2 = Union[AdvancedColors2String, AdvancedColors2Number, AdvancedColors2NumberArray, AdvancedColors2ReallyCoolType]
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from __future__ import annotations

from enum import Enum
from pydantic import BaseModel
from typing import Literal, Union


class SomeEnumTypes(str, Enum):
A = "A"
C = "C"

class SomeEnumA(BaseModel):
type: Literal[SomeEnumTypes.A] = SomeEnumTypes.A

class SomeEnumC(BaseModel):
type: Literal[SomeEnumTypes.C] = SomeEnumTypes.C
content: int

SomeEnum = Union[SomeEnumA, SomeEnumC]
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from __future__ import annotations

from enum import Enum
from pydantic import BaseModel
from typing import Literal, Union


class AutofilledByUsInner(BaseModel):
"""
Generated type representing the anonymous struct variant `Us` of the `AutofilledBy` Rust enum
"""
uuid: str
"""
The UUID for the fill
"""

class AutofilledBySomethingElseInner(BaseModel):
"""
Generated type representing the anonymous struct variant `SomethingElse` of the `AutofilledBy` Rust enum
"""
uuid: str
"""
The UUID for the fill
"""

class AutofilledByTypes(str, Enum):
US = "Us"
SOMETHING_ELSE = "SomethingElse"

class AutofilledByUs(BaseModel):
"""
This field was autofilled by us
"""
type: Literal[AutofilledByTypes.US] = AutofilledByTypes.US
content: AutofilledByUsInner

class AutofilledBySomethingElse(BaseModel):
"""
Something else autofilled this field
"""
type: Literal[AutofilledByTypes.SOMETHING_ELSE] = AutofilledByTypes.SOMETHING_ELSE
content: AutofilledBySomethingElseInner

# Enum keeping track of who autofilled a field
AutofilledBy = Union[AutofilledByUs, AutofilledBySomethingElse]
Loading

0 comments on commit b6e6bf1

Please sign in to comment.