diff --git a/CHANGELOG.md b/CHANGELOG.md index cc5bbb9..942d9c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,10 +17,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Complex control flow structures - Records (Java 14+) - File I/O operations +- Enhanced TypeName support for common Java library types (List, Map, Set, etc.) +- Better Python type mapping (bool -> boolean, int -> int, etc.) +- Added `add_javadoc` method alongside existing `add_javadoc_line` +- Added `add_raw_line` method for raw code with newlines +- Support for handling None values in TypeName.get() ### Changed - Updated README structure with clear Python-to-Java code mappings - Enhanced documentation with working examples +- Improved TypeName.get() to handle Python types directly (e.g., `TypeName.get(bool)` returns `TypeName.BOOLEAN`) +- Updated method API: `add_code()` renamed to `add_raw_code()` for clarity +- Enhanced JavaDoc generation with better newline handling +- Fixed annotation newline formatting issues +- Improved static class name handling + +### Fixed +- TypeName.get() now properly handles Python type objects instead of just type names +- JavaDoc emission now properly handles newlines and prefixes +- Annotation formatting with correct newline placement +- Primitive type boxing to proper wrapper classes (e.g., int -> java.lang.Integer) ## [0.1.0] - 2025-07-29 @@ -128,3 +144,8 @@ and the following files were added: - Added begin_statement_chain, add_chained_item, and end_statement_chain to MethodSpec builder - Includes corresponding changes in CodeBlock - Switched add_javadoc to add_javadoc_line +- Added more default types to TypeName + +### Fixes +- Fixed annotation newline issue +- Fixed passing in python types to give you a TypeName diff --git a/README.md b/README.md index c795cb9..318f462 100644 --- a/README.md +++ b/README.md @@ -977,8 +977,8 @@ print(str(java_file)) # Java file with imports and file comment java_file_with_imports = JavaFile.builder("com.example", simple_class) \ - .add_file_comment("This is a generated file.") \ - .add_file_comment("Do not edit manually.") \ + .add_file_comment_line("This is a generated file.") \ + .add_file_comment_line("Do not edit manually.") \ .add_static_import(ClassName.get("java.lang", "System"), "out") \ .build() print(str(java_file_with_imports)) @@ -1026,6 +1026,7 @@ method = MethodSpec.method_builder("example") \ ``` ## TODOs +I think of these as nice-ities, but they are more about convenience rather than correctness. There are work-arounds/other tools that can be used to create these desired affects. 1. TreeSitter API to synactically validate java file 2. Add kwargs to method spec builder. Currently code block will have an issue of overwriting previous @@ -1035,8 +1036,7 @@ method = MethodSpec.method_builder("example") \ 5. Name Allocator if we so desire (?) 6. Annotation member has to be valid java identifier 7. Handle primitive types better in ClassName i.e. validation -8. Improve tests with exact output strings and also slim down unneeded tests -9. Pass in TypeSpec for Types as well (for nested classes) ? It might work and we can include a self key too +8. Pass in TypeSpec for Types as well (for nested classes) ? It might work and we can include a self key too ## License diff --git a/examples/complex_example.py b/examples/complex_example.py index 098b7fe..db8c997 100644 --- a/examples/complex_example.py +++ b/examples/complex_example.py @@ -115,7 +115,7 @@ def main(): # Create the Java file java_file = ( JavaFile.builder("com.example.processor", processor) - .add_file_comment("This is a generated file. Do not edit!") + .add_file_comment_line("This is a generated file. Do not edit!") .build() ) diff --git a/pyjavapoet/annotation_spec.py b/pyjavapoet/annotation_spec.py index 126ae12..f9e8de2 100644 --- a/pyjavapoet/annotation_spec.py +++ b/pyjavapoet/annotation_spec.py @@ -15,13 +15,6 @@ Modified by Matthew Au-Yeung on 2025-07-29; see changelog.md for more details. - Similar APIs ported from Java to Python. - -Changes and Current API: -- The API is modeled after JavaPoet's AnnotationSpec, but adapted for Python. -- AnnotationSpec is immutable; use the builder to create new instances. -- Supports representing Java annotations for classes, methods, fields, parameters, etc. -- The main API: - - AnnotationSpec(type_name, members) """ from typing import Any, Union diff --git a/pyjavapoet/code_block.py b/pyjavapoet/code_block.py index 27c3c76..b935b6a 100644 --- a/pyjavapoet/code_block.py +++ b/pyjavapoet/code_block.py @@ -15,11 +15,6 @@ Modified by Matthew Au-Yeung on 2025-07-29; see changelog.md for more details. - Similar APIs ported from Java to Python. - -Changes and Current API: -- The API is modeled after JavaPoet's CodeBlock, but adapted for Python. -- CodeBlock is immutable; use the builder to create new instances. -- Supports formatting Java code with placeholders. """ import re @@ -51,7 +46,7 @@ class CodeBlock(Code["CodeBlock"]): placeholder_match = re.compile( r""" \$( - (?P[LSTN<>]) # $L, $S, $T, $N, $<, $> + (?P[LSTN<>]) # $L, $S, $T, $N, $<, $> | # or (?P[a-zA-Z_][a-zA-Z0-9_]*) # $name : # : @@ -64,6 +59,26 @@ class CodeBlock(Code["CodeBlock"]): re.VERBOSE, ) + placeholder_match_with_newlines = re.compile( + r""" + ( + \$( + (?P[LSTN<>]) # $L, $S, $T, $N, $<, $> + | # or + (?P[a-zA-Z_][a-zA-Z0-9_]*) # $name + : # : + (?P[LSTN]) # T, L, S, N + | # or + (?P\d+) # $1, $2, etc. + (?P[LSTN<>]) # L, S, T, N, $<, $> + ) + | + (\n) # or a literal newline + ) + """, + re.VERBOSE, + ) + def __init__(self, format_parts: list[str], args: list[Any], named_args: dict[str, Any]): self.format_parts = format_parts self.args = args @@ -140,9 +155,10 @@ def emit(self, code_writer: "CodeWriter", new_line_prefix: str = "") -> None: code_writer.emit(part, new_line_prefix) def emit_javadoc(self, code_writer: "CodeWriter") -> None: - code_writer.emit("/**\n * ") + code_writer.emit("/**\n") self.emit(code_writer, " * ") - code_writer.emit("\n */") + code_writer.emit("\n", " * ") + code_writer.emit(" */") def javadoc(self) -> str: writer = CodeWriter() @@ -178,6 +194,13 @@ def join_to_code(code_blocks: list["CodeBlock"], separator: str = "") -> "CodeBl @staticmethod def add_javadoc(javadoc: Optional["CodeBlock"], format_string: str, *args) -> "CodeBlock": + if javadoc: + return CodeBlock.join_to_code([javadoc, CodeBlock.of(format_string, *args)]) + else: + return CodeBlock.of(format_string, *args) + + @staticmethod + def add_javadoc_line(javadoc: Optional["CodeBlock"], format_string: str, *args) -> "CodeBlock": if javadoc: return CodeBlock.join_to_code([javadoc, CodeBlock.of(format_string, *args)], "\n") else: @@ -207,7 +230,7 @@ def __init__( def add(self, format_string: str, *args, **kwargs) -> "CodeBlock.Builder": # Check for arguments in the format string - matches = list(re.finditer(CodeBlock.placeholder_match, format_string)) + matches = list(re.finditer(CodeBlock.placeholder_match_with_newlines, format_string)) # Simple case: no arguments if not matches: @@ -248,6 +271,11 @@ def add_statement(self, format_string: str, *args, **kwargs) -> "CodeBlock.Build self.add(format_string, *args, **kwargs) self.add(";\n") return self + + def add_line(self, format_string: str, *args, **kwargs) -> "CodeBlock.Builder": + self.add(format_string, *args, **kwargs) + self.add("\n") + return self def begin_statement(self, format_string: str, *args, **kwargs) -> "CodeBlock.Builder": parts = format_string.split("\n") diff --git a/pyjavapoet/code_writer.py b/pyjavapoet/code_writer.py index 875c3bd..36eec15 100644 --- a/pyjavapoet/code_writer.py +++ b/pyjavapoet/code_writer.py @@ -15,11 +15,6 @@ Modified by Matthew Au-Yeung on 2025-07-29; see changelog.md for more details. - Similar APIs ported from Java to Python. - -Changes and Current API: -- The API is modeled after JavaPoet's CodeWriter, but adapted for Python. -- CodeWriter is immutable; use the builder to create new instances. -- Supports emitting Java code with proper formatting. """ from typing import Annotated, Literal @@ -80,6 +75,10 @@ def unindent(self, count: int = 1) -> None: def emit(self, s: str | Constant, new_line_prefix: str = "") -> "CodeWriter": if s.startswith("\n"): + if self.__line_start and new_line_prefix: + self.__out.append(self.__indent * self.__indent_level) + self.__out.append(new_line_prefix) + # Reset line start self.__out.append("\n") self.__line_start = True diff --git a/pyjavapoet/field_spec.py b/pyjavapoet/field_spec.py index 9811a50..fdf3f5e 100644 --- a/pyjavapoet/field_spec.py +++ b/pyjavapoet/field_spec.py @@ -15,12 +15,6 @@ Modified by Matthew Au-Yeung on 2025-07-29; see changelog.md for more details. - Similar APIs ported from Java to Python. - -Changes and Current API: -- The API is modeled after JavaPoet's FieldSpec, but adapted for Python. -- FieldSpec is immutable; use the builder to create new instances. -- Supports Java modifiers (from Modifier), annotations (AnnotationSpec), type information (TypeName), - and initializer (CodeBlock). """ from typing import TYPE_CHECKING, Optional, Union @@ -28,10 +22,10 @@ from pyjavapoet.annotation_spec import AnnotationSpec from pyjavapoet.code_base import Code from pyjavapoet.code_block import CodeBlock -from pyjavapoet.code_writer import CodeWriter +from pyjavapoet.code_writer import EMPTY_STRING, CodeWriter from pyjavapoet.modifier import Modifier from pyjavapoet.type_name import TypeName -from pyjavapoet.util import deep_copy +from pyjavapoet.util import deep_copy, throw_if_invalid_java_identifier if TYPE_CHECKING: from pyjavapoet.code_writer import CodeWriter @@ -98,69 +92,9 @@ def to_builder(self) -> "Builder": deep_copy(self.initializer), ) - @staticmethod - def is_valid_field_name(name: str) -> bool: - java_keywords = { - "abstract", - "assert", - "boolean", - "break", - "byte", - "case", - "catch", - "char", - "class", - "const", - "continue", - "default", - "do", - "double", - "else", - "enum", - "extends", - "final", - "finally", - "float", - "for", - "goto", - "if", - "implements", - "import", - "instanceof", - "int", - "interface", - "long", - "native", - "new", - "package", - "private", - "protected", - "public", - "return", - "short", - "static", - "strictfp", - "super", - "switch", - "synchronized", - "this", - "throw", - "throws", - "transient", - "try", - "void", - "volatile", - "while", - "true", - "false", - "null", - } - return name.isidentifier() and name not in java_keywords - @staticmethod def builder(type_name: Union["TypeName", str, type], name: str) -> "Builder": - if not FieldSpec.is_valid_field_name(name): - raise ValueError(f"Invalid field name: {name}") + throw_if_invalid_java_identifier(name) if not isinstance(type_name, TypeName): type_name = TypeName.get(type_name) @@ -207,6 +141,10 @@ def add_annotation(self, annotation_spec: "AnnotationSpec") -> "FieldSpec.Builde def add_javadoc(self, format_string: str, *args) -> "FieldSpec.Builder": self.__javadoc = CodeBlock.add_javadoc(self.__javadoc, format_string, *args) return self + + def add_javadoc_line(self, format_string: str = EMPTY_STRING, *args) -> "FieldSpec.Builder": + self.__javadoc = CodeBlock.add_javadoc_line(self.__javadoc, format_string, *args) + return self def initializer(self, format_string: str | CodeBlock, *args) -> "FieldSpec.Builder": if isinstance(format_string, str): diff --git a/pyjavapoet/java_file.py b/pyjavapoet/java_file.py index c2478d6..2e0f998 100644 --- a/pyjavapoet/java_file.py +++ b/pyjavapoet/java_file.py @@ -15,13 +15,6 @@ Modified by Matthew Au-Yeung on 2025-07-29; see changelog.md for more details. - Similar APIs ported from Java to Python. - -Changes and Current API: -- The API is modeled after JavaPoet's JavaFile, but adapted for Python. -- JavaFile is immutable; use the builder to create new instances. -- Supports package declaration, imports, and type declarations. -- The main API: - - JavaFile(package_name, type_spec, file_comment, indent, static_imports) """ import sys @@ -188,16 +181,13 @@ def __init__( self.__indent = indent self.__static_imports = static_imports or {} - def add_generated_by(self, creator: str, extra_comment: str = "", *args) -> "JavaFile.Builder": - self.add_file_comment("@generated") - self.add_file_comment(f"Generated by {creator}", *args) - if extra_comment: - self.add_file_comment(extra_comment, *args) - return self - def add_file_comment(self, format_string: str = EMPTY_STRING, *args) -> "JavaFile.Builder": self.__file_comment = CodeBlock.add_javadoc(self.__file_comment, format_string, *args) return self + + def add_file_comment_line(self, format_string: str = EMPTY_STRING, *args) -> "JavaFile.Builder": + self.__file_comment = CodeBlock.add_javadoc_line(self.__file_comment, format_string, *args) + return self def indent(self, indent: str) -> "JavaFile.Builder": self.__indent = indent diff --git a/pyjavapoet/method_spec.py b/pyjavapoet/method_spec.py index 0e9cab5..70ac8e2 100644 --- a/pyjavapoet/method_spec.py +++ b/pyjavapoet/method_spec.py @@ -15,19 +15,6 @@ Modified by Matthew Au-Yeung on 2025-07-29; see changelog.md for more details. - Similar APIs ported from Java to Python. - -Changes and Current API: -- The API is modeled after JavaPoet's MethodSpec, but adapted for Python. -- MethodSpec is immutable; use the builder pattern to create instances. -- Supports Java modifiers (from Modifier), annotations (AnnotationSpec), type variables, parameters (ParameterSpec), - exceptions, return type (TypeName), and method/constructor body (CodeBlock). -- The main API: - - MethodSpec(name, modifiers, parameters, return_type, exceptions, type_variables, javadoc, annotations, - code, default_value, kind) - - .emit(code_writer): emits the method or constructor as Java code. - - .to_builder(): returns a builder initialized with this method's values. - - MethodSpec.builder(name): static method to start building a method. -- Utility functions and validation are used to ensure correctness. """ from enum import Enum, auto @@ -277,16 +264,20 @@ def add_exception(self, exception: Union["TypeName", str, type]) -> "MethodSpec. def add_type_variable(self, type_variable: "TypeVariableName") -> "MethodSpec.Builder": self.__type_variables.append(type_variable) return self + + def add_javadoc(self, format_string: str, *args) -> "MethodSpec.Builder": + self.__javadoc = CodeBlock.add_javadoc(self.__javadoc, format_string, *args) + return self def add_javadoc_line(self, format_string: str = EMPTY_STRING, *args) -> "MethodSpec.Builder": - self.__javadoc = CodeBlock.add_javadoc(self.__javadoc, format_string, *args) + self.__javadoc = CodeBlock.add_javadoc_line(self.__javadoc, format_string, *args) return self def add_annotation(self, annotation_spec: "AnnotationSpec") -> "MethodSpec.Builder": self.__annotations.append(annotation_spec) return self - def add_code(self, format_string: str, *args) -> "MethodSpec.Builder": + def add_raw_code(self, format_string: str, *args) -> "MethodSpec.Builder": if self.__kind == MethodSpec.Kind.COMPACT_CONSTRUCTOR: raise ValueError("Compact constructors cannot have a body") @@ -294,6 +285,14 @@ def add_code(self, format_string: str, *args) -> "MethodSpec.Builder": self.__code_builder.add(format_string, *args) return self + def add_raw_line(self, format_string: str, *args) -> "MethodSpec.Builder": + if self.__kind == MethodSpec.Kind.COMPACT_CONSTRUCTOR: + raise ValueError("Compact constructors cannot have a body") + + if self.__code_builder is not None: + self.__code_builder.add_line(format_string, *args) + return self + def add_statement(self, format_string: str, *args) -> "MethodSpec.Builder": if self.__kind == MethodSpec.Kind.COMPACT_CONSTRUCTOR: raise ValueError("Compact constructors cannot have a body") diff --git a/pyjavapoet/parameter_spec.py b/pyjavapoet/parameter_spec.py index 4c4c4df..501e316 100644 --- a/pyjavapoet/parameter_spec.py +++ b/pyjavapoet/parameter_spec.py @@ -15,19 +15,6 @@ Modified by Matthew Au-Yeung on 2025-07-29; see changelog.md for more details. - Similar APIs ported from Java to Python. - -This module defines the ParameterSpec class, which represents a parameter for a Java method or constructor. - -Changes and Current API: -- The API is modeled after JavaPoet's ParameterSpec, but adapted for Python. -- ParameterSpec is immutable; use the builder pattern to create instances. -- Supports Java modifiers (from Modifier), annotations (AnnotationSpec), varargs, and type information (TypeName). -- The main API: - - ParameterSpec(type_name, name, modifiers, annotations, varargs=False) - - .emit(code_writer): emits the parameter as Java code. - - .to_builder(): returns a builder initialized with this parameter's values. - - ParameterSpec.builder(type_name, name): static method to start building a parameter. -- Utility functions and validation (e.g., throw_if_invalid_java_identifier) are used to ensure correctness. """ import re diff --git a/pyjavapoet/type_name.py b/pyjavapoet/type_name.py index 323c017..d79d455 100644 --- a/pyjavapoet/type_name.py +++ b/pyjavapoet/type_name.py @@ -15,16 +15,6 @@ Modified by Matthew Au-Yeung on 2025-07-29; see changelog.md for more details. - Similar APIs ported from Java to Python. - -Classes representing Java types in PyPoet (Note some files collapsed into this one). - -This module defines classes for representing Java types: -- TypeName: Base class for all types -- ClassName: Represents a class or interface type -- ArrayTypeName: Represents an array type -- ParameterizedTypeName: Represents a type with generic arguments -- TypeVariableName: Represents a type variable (generic type parameter) -- WildcardTypeName: Represents a wildcard type (e.g., ? extends Number) """ from abc import ABC, abstractmethod @@ -50,12 +40,51 @@ class TypeName(ABC): FLOAT: "ClassName" SHORT: "ClassName" BYTE: "ClassName" - CHARACTER: "ClassName" + CHAR: "ClassName" BOOLEAN: "ClassName" VOID: "ClassName" OBJECT: "ClassName" STRING: "ClassName" VOID: "ClassName" + LIST: "ClassName" + MAP: "ClassName" + SET: "ClassName" + + # Commonly used Java library types + LIST: "ClassName" + MAP: "ClassName" + SET: "ClassName" + COLLECTION: "ClassName" + ITERABLE: "ClassName" + ITERATOR: "ClassName" + OPTIONAL: "ClassName" + STREAM: "ClassName" + ARRAY_LIST: "ClassName" + LINKED_LIST: "ClassName" + HASH_MAP: "ClassName" + LINKED_HASH_MAP: "ClassName" + TREE_MAP: "ClassName" + HASH_SET: "ClassName" + LINKED_HASH_SET: "ClassName" + TREE_SET: "ClassName" + QUEUE: "ClassName" + DEQUE: "ClassName" + STACK: "ClassName" + VECTOR: "ClassName" + ENUM_SET: "ClassName" + ENUM_MAP: "ClassName" + BIG_DECIMAL: "ClassName" + BIG_INTEGER: "ClassName" + DATE: "ClassName" + CALENDAR: "ClassName" + LOCALE: "ClassName" + FILE: "ClassName" + PATH: "ClassName" + URL: "ClassName" + UUID: "ClassName" + OBJECTS: "ClassName" + STRING_BUILDER: "ClassName" + STRING_BUFFER: "ClassName" # Primitive types mapping PRIMITIVE_TYPES = { @@ -148,7 +177,10 @@ def is_any_primitive(self) -> bool: return False @staticmethod - def get(type_mirror_or_name: Union[str, type, "TypeName"]) -> "TypeName": + def get(type_mirror_or_name: Union[str, type, "TypeName", None]) -> "TypeName": + if not type_mirror_or_name: + return TypeName.VOID + if isinstance(type_mirror_or_name, TypeName): return type_mirror_or_name @@ -163,22 +195,20 @@ def get(type_mirror_or_name: Union[str, type, "TypeName"]) -> "TypeName": # Handle Python types if isinstance(type_mirror_or_name, type): - type_name = type_mirror_or_name.__name__ # Map Python types to Java types type_mapping = { - "bool": "boolean", - "int": "int", - "float": "float", - "str": "java.lang.String", - "list": "java.util.List", - "dict": "java.util.Map", - "set": "java.util.Set", - "tuple": "java.util.List", - "None": "void", + bool: TypeName.BOOLEAN, + int: TypeName.INTEGER, + float: TypeName.FLOAT, + str: TypeName.STRING, + list: TypeName.LIST, + dict: TypeName.MAP, + set: TypeName.SET, + tuple: TypeName.LIST } - if type_name in type_mapping: - return TypeName.get(type_mapping[type_name]) + if type_mirror_or_name in type_mapping: + return TypeName.get(type_mapping[type_mirror_or_name]) else: # Default to Java Object for other Python types return TypeName.OBJECT @@ -225,15 +255,11 @@ def array(self) -> "ArrayTypeName": def to_type_param(self) -> "TypeName": if self.is_primitive(): - return ClassName(self.package_name, [TypeName.PRIMITIVE_TYPES[self.simple_name]]) + boxed_name = TypeName.PRIMITIVE_TYPES[self.simple_name] + package_name = TypeName.ALL_PRIMITIVE_TYPES[boxed_name] + return ClassName.get(package_name, boxed_name) return self - def __ignore_import(self) -> bool: - """ - Ignore IFF its a primitive type or a boxed primitive type - """ - return self.nested_name in TypeName.ALL_PRIMITIVE_TYPES - @property def reflection_name(self) -> str: if not self.package_name: @@ -521,14 +547,48 @@ def supertypes_of(*lower_bounds: Union["TypeName", str, type]) -> "WildcardTypeN return WildcardTypeName(lower_bounds=[TypeName.get(bound) for bound in lower_bounds]) -TypeName.INTEGER = ClassName.get("", "Integer") -TypeName.LONG = ClassName.get("", "Long") -TypeName.DOUBLE = ClassName.get("", "Double") -TypeName.FLOAT = ClassName.get("", "Float") -TypeName.SHORT = ClassName.get("", "Short") -TypeName.BYTE = ClassName.get("", "Byte") -TypeName.CHARACTER = ClassName.get("", "Character") -TypeName.BOOLEAN = ClassName.get("", "Boolean") -TypeName.VOID = ClassName.get("", "Void") -TypeName.OBJECT = ClassName.get("", "Object") -TypeName.STRING = ClassName.get("", "String") +TypeName.INTEGER = ClassName.get("", "int") +TypeName.LONG = ClassName.get("", "long") +TypeName.DOUBLE = ClassName.get("", "double") +TypeName.FLOAT = ClassName.get("", "float") +TypeName.SHORT = ClassName.get("", "short") +TypeName.BYTE = ClassName.get("", "byte") +TypeName.CHAR = ClassName.get("", "char") +TypeName.BOOLEAN = ClassName.get("", "boolean") +TypeName.VOID = ClassName.get("", "void") +TypeName.OBJECT = ClassName.get("java.lang", "Object") +TypeName.STRING = ClassName.get("java.lang", "String") +TypeName.LIST = ClassName.get("java.util", "List") +TypeName.MAP = ClassName.get("java.util", "Map") +TypeName.SET = ClassName.get("java.util", "Set") +TypeName.COLLECTION = ClassName.get("java.util", "Collection") +TypeName.ITERABLE = ClassName.get("java.lang", "Iterable") +TypeName.ITERATOR = ClassName.get("java.util", "Iterator") +TypeName.OPTIONAL = ClassName.get("java.util", "Optional") +TypeName.STREAM = ClassName.get("java.util.stream", "Stream") +TypeName.ARRAY_LIST = ClassName.get("java.util", "ArrayList") +TypeName.LINKED_LIST = ClassName.get("java.util", "LinkedList") +TypeName.HASH_MAP = ClassName.get("java.util", "HashMap") +TypeName.LINKED_HASH_MAP = ClassName.get("java.util", "LinkedHashMap") +TypeName.TREE_MAP = ClassName.get("java.util", "TreeMap") +TypeName.HASH_SET = ClassName.get("java.util", "HashSet") +TypeName.LINKED_HASH_SET = ClassName.get("java.util", "LinkedHashSet") +TypeName.TREE_SET = ClassName.get("java.util", "TreeSet") +TypeName.QUEUE = ClassName.get("java.util", "Queue") +TypeName.DEQUE = ClassName.get("java.util", "Deque") +TypeName.STACK = ClassName.get("java.util", "Stack") +TypeName.VECTOR = ClassName.get("java.util", "Vector") +TypeName.ENUM_SET = ClassName.get("java.util", "EnumSet") +TypeName.ENUM_MAP = ClassName.get("java.util", "EnumMap") +TypeName.BIG_DECIMAL = ClassName.get("java.math", "BigDecimal") +TypeName.BIG_INTEGER = ClassName.get("java.math", "BigInteger") +TypeName.DATE = ClassName.get("java.util", "Date") +TypeName.CALENDAR = ClassName.get("java.util", "Calendar") +TypeName.LOCALE = ClassName.get("java.util", "Locale") +TypeName.FILE = ClassName.get("java.io", "File") +TypeName.PATH = ClassName.get("java.nio.file", "Path") +TypeName.URL = ClassName.get("java.net", "URL") +TypeName.UUID = ClassName.get("java.util", "UUID") +TypeName.OBJECTS = ClassName.get("java.util", "Objects") +TypeName.STRING_BUILDER = ClassName.get("java.lang", "StringBuilder") +TypeName.STRING_BUFFER = ClassName.get("java.lang", "StringBuffer") diff --git a/pyjavapoet/type_spec.py b/pyjavapoet/type_spec.py index 337dc0b..7481fe4 100644 --- a/pyjavapoet/type_spec.py +++ b/pyjavapoet/type_spec.py @@ -15,15 +15,6 @@ Modified by Matthew Au-Yeung on 2025-07-29; see changelog.md for more details. - Similar APIs ported from Java to Python. - -TypeSpec for representing Java classes, interfaces, enums, and annotations. - -This module defines the TypeSpec class, which is used to represent Java types: -- Classes -- Interfaces -- Enums -- Annotations -- Records """ from enum import Enum, auto @@ -155,6 +146,7 @@ def emit(self, code_writer: "CodeWriter") -> None: # Emit Javadoc if self.javadoc: self.javadoc.emit_javadoc(code_writer) + code_writer.emit("\n") # Emit annotations for annotation in self.annotations: @@ -446,9 +438,13 @@ def add_permitted_subclass(self, subclass: Union["TypeName", str, type]) -> "Typ self.__permitted_subclasses.append(subclass) return self + + def add_javadoc(self, format_string: str, *args) -> "TypeSpec.Builder": + self.__javadoc = CodeBlock.add_javadoc(self.__javadoc, format_string, *args) + return self def add_javadoc_line(self, format_string: str = EMPTY_STRING, *args) -> "TypeSpec.Builder": - self.__javadoc = CodeBlock.add_javadoc(self.__javadoc, format_string, *args) + self.__javadoc = CodeBlock.add_javadoc_line(self.__javadoc, format_string, *args) return self def add_annotation(self, annotation_spec: AnnotationSpec) -> "TypeSpec.Builder": diff --git a/tests/test_code_block.py b/tests/test_code_block.py index 10a4217..db140c1 100644 --- a/tests/test_code_block.py +++ b/tests/test_code_block.py @@ -325,11 +325,24 @@ def test_emit_as_java_doc(self): def test_add_javadoc(self): """Test adding JavaDoc to a code block.""" block = CodeBlock.of("$L", "Hello, World!") - block = CodeBlock.add_javadoc(block, "Hello, World!") + block = CodeBlock.add_javadoc(block, " Hello, World!\n\n") writer = CodeWriter() block.emit_javadoc(writer) - self.assertEqual(str(writer), "/**\n * Hello, World!\n * Hello, World!\n */") + print(str(writer)) + self.assertEqual(str(writer), "/**\n * Hello, World! Hello, World!\n * \n * \n */") + def test_add_javadoc_with_line_breaks(self): + """Test adding JavaDoc with line breaks.""" + block = CodeBlock.add_javadoc_line(None, "One!\n") + block = CodeBlock.add_javadoc_line(block, "Two!\nThree!\nFour!") + writer = CodeWriter() + block.emit_javadoc(writer) + self.assertEqual(str(writer), "/**\n * One!\n * \n * Two!\n * Three!\n * Four!\n */") + + def test_add_raw_line(self): + """Test adding raw code to a code block.""" + block = CodeBlock.builder().add_line("System.out.println($S);", "Hello, World!").build() + self.assertEqual(str(block), 'System.out.println("Hello, World!");\n') if __name__ == "__main__": unittest.main() diff --git a/tests/test_code_writer.py b/tests/test_code_writer.py index 333ec87..ef88d0e 100644 --- a/tests/test_code_writer.py +++ b/tests/test_code_writer.py @@ -55,6 +55,24 @@ def test_indentation_handling(self): expected = "start\n indented\n double indented\n single indented\nend\n" self.assertEqual(result, expected) + def test_emit_with_nested_newlines_handling(self): + """Test that if double newline and there's a new_line_prefix, we add it.""" + writer = CodeWriter() + # Emit a line, then a double newline with a prefix, then another line + writer.emit("line1\n", new_line_prefix="// ") + writer.emit("\n\nline2\n", new_line_prefix="// ") + writer.emit("line3", new_line_prefix="// ") + + result = str(writer) + # The expected result: + # // line1 + # // + # // + # // line2 + # // line3 + expected = "// line1\n// \n// \n// line2\n// line3" + self.assertEqual(result, expected) + def test_custom_indent_string(self): """Test custom indentation string.""" writer = CodeWriter(indent="\t") diff --git a/tests/test_complex.py b/tests/test_complex.py index 872e4f6..74d5cfb 100644 --- a/tests/test_complex.py +++ b/tests/test_complex.py @@ -107,7 +107,7 @@ def test_complex_example(self): # Create the Java file java_file = ( JavaFile.builder("com.example.processor", processor) - .add_file_comment("This is a generated file. Do not edit!") + .add_file_comment_line("This is a generated file. Do not edit!") .build() ) diff --git a/tests/test_field_spec.py b/tests/test_field_spec.py index 53aaa74..c7840bb 100644 --- a/tests/test_field_spec.py +++ b/tests/test_field_spec.py @@ -82,8 +82,8 @@ def test_field_with_javadoc(self): """Test field with javadoc.""" field = ( FieldSpec.builder(ClassName.get("java.lang", "String"), "name") - .add_javadoc("@deprecated Use something else") - .add_javadoc("Use something else") + .add_javadoc_line("@deprecated Use something else") + .add_javadoc_line("Use something else") .build() ) diff --git a/tests/test_java_file.py b/tests/test_java_file.py index f241859..7c1bff6 100644 --- a/tests/test_java_file.py +++ b/tests/test_java_file.py @@ -188,8 +188,8 @@ def test_file_comment(self): type_spec = TypeSpec.class_builder("Test").build() java_file = ( JavaFile.builder("com.example", type_spec) - .add_file_comment("This is a generated file.") - .add_file_comment("Do not modify directly.") + .add_file_comment_line("This is a generated file.") + .add_file_comment_line("Do not modify directly.") .build() ) @@ -435,7 +435,6 @@ def test_inner_inner_scope_conflicts_should_use_canonical_in_inner_scope(self): java_file = JavaFile.builder("com.example", top_class).indent(" ").build() result = str(java_file) - print(result) self.assertEqual( result, """\ @@ -682,23 +681,31 @@ def test_file_with_comments(self): method = ( MethodSpec.method_builder("documented") .add_javadoc_line("This is a documented method.") + .add_javadoc_line() .add_javadoc_line("@param none no parameters") .add_javadoc_line("@return nothing") .add_modifiers(Modifier.PUBLIC) .returns("void") - .add_statement("// Single line comment") - .add_statement("/* Block comment */") + .add_raw_line("// Single line comment") + .add_raw_line("/* Block comment */") .build() ) type_spec = ( TypeSpec.class_builder("Commented") - .add_javadoc_line("This is a documented class.\n") + .add_javadoc_line("This is a documented class.") .add_method(method) .build() ) - java_file = JavaFile.builder("com.example", type_spec).add_generated_by("pyjavapoet").build() + java_file = ( + JavaFile.builder("com.example", type_spec) + .indent(" ") + .add_file_comment_line("This is a documented file.\n") + .add_file_comment_line("@generated") + .add_file_comment_line("Generated by pyjavapoet") + .build() + ) file_path = java_file.write_to_dir(self.temp_dir) @@ -706,13 +713,32 @@ def test_file_with_comments(self): with open(file_path, "r", encoding="utf-8") as f: content = f.read() - self.assertIn(" * @generated", content) - self.assertIn(" * Generated by pyjavapoet", content) - self.assertIn("/**", content) - self.assertIn("This is a documented class.", content) - self.assertIn("This is a documented method.", content) - self.assertIn("// Single line comment", content) - self.assertIn("/* Block comment */", content) + expected = """\ +/** + * This is a documented file. + * + * @generated + * Generated by pyjavapoet + */ +package com.example; + +/** + * This is a documented class. + */ +class Commented { + /** + * This is a documented method. + * + * @param none no parameters + * @return nothing + */ + public void documented() { + // Single line comment + /* Block comment */ + } +} +""" + self.assertEqual(content, expected) def test_large_file_handling(self): """Test handling of larger files.""" diff --git a/tests/test_type_name.py b/tests/test_type_name.py index 2234aaa..d407482 100644 --- a/tests/test_type_name.py +++ b/tests/test_type_name.py @@ -103,6 +103,19 @@ def test_equals_and_hash_code_wildcard_type_name(self): self.assertEqual(hash(a), hash(b)) self.assertNotEqual(a, c) + def test_get_with_type_or_none(self): + """Test get with type or none.""" + self.assertEqual(TypeName.get(bool), TypeName.BOOLEAN) + self.assertEqual(TypeName.get(int), TypeName.INTEGER) + self.assertEqual(TypeName.get(float), TypeName.FLOAT) + self.assertEqual(TypeName.get(str), TypeName.STRING) + self.assertEqual(TypeName.get(list), TypeName.LIST) + self.assertEqual(TypeName.get(dict), TypeName.MAP) + self.assertEqual(TypeName.get(set), TypeName.SET) + self.assertEqual(TypeName.get(tuple), TypeName.LIST) + self.assertEqual(TypeName.get(None), TypeName.VOID) + + def test_is_primitive(self): """Test primitive type detection.""" self.assertTrue(TypeName.get("boolean").is_primitive()) @@ -137,9 +150,21 @@ def test_can_box_annotated_primitive(self): """Test boxing annotated primitive types.""" # This would test annotation preservation during boxing # For now, just test basic boxing functionality - int_type = TypeName.INTEGER - boxed = int_type.to_type_param() - self.assertEqual(boxed, ClassName.get("java.lang", "Integer")) + types = [ + (TypeName.INTEGER, ClassName.get("java.lang", "Integer")), + (TypeName.BOOLEAN, ClassName.get("java.lang", "Boolean")), + (TypeName.FLOAT, ClassName.get("java.lang", "Float")), + (TypeName.LONG, ClassName.get("java.lang", "Long")), + (TypeName.SHORT, ClassName.get("java.lang", "Short")), + (TypeName.DOUBLE, ClassName.get("java.lang", "Double")), + (TypeName.VOID, ClassName.get("java.lang", "Void")), + (TypeName.CHAR, ClassName.get("java.lang", "Character")), + (TypeName.BYTE, ClassName.get("java.lang", "Byte")), + ] + for t, c in types: + boxed = t.to_type_param() + print(boxed) + self.assertEqual(boxed, c) def test_array_type_creation(self): """Test array type creation.""" @@ -177,6 +202,24 @@ def test_parameterized_type_with_wildcards(self): self.assertEqual(str(list_wildcard), "List") + def test_parameterized_type_with_primitives(self): + types = [ + (TypeName.INTEGER, ClassName.get("java.lang", "Integer")), + (TypeName.BOOLEAN, ClassName.get("java.lang", "Boolean")), + (TypeName.FLOAT, ClassName.get("java.lang", "Float")), + (TypeName.LONG, ClassName.get("java.lang", "Long")), + (TypeName.SHORT, ClassName.get("java.lang", "Short")), + (TypeName.DOUBLE, ClassName.get("java.lang", "Double")), + (TypeName.VOID, ClassName.get("java.lang", "Void")), + (TypeName.CHAR, ClassName.get("java.lang", "Character")), + (TypeName.BYTE, ClassName.get("java.lang", "Byte")), + ] + list_type = ClassName.get("java.util", "List") + for t, c in types: + new_type = list_type.with_type_arguments(t) + self.assertIn(c.simple_name, str(new_type)) + self.assertNotIn(t.simple_name, str(new_type)) + def test_nested_parameterized_types(self): """Test deeply nested parameterized types.""" map_type = ClassName.get("java.util", "Map").with_type_arguments(